How to Create a Programmatic Problem¶
A programmatic problem is assessed using unit tests, meaning you’ll write code to test your students' submitted code. Unit tests allow for more versatile and powerful testing, but they require a deeper understanding of programming and are recommended for teachers who are comfortable with coding.
In this example, we’ll ask the student to create a calculator function that accepts three inputs: a string or character (+
, -
, *
, or /
), an integer, and another integer. The function is expected to return the result of the operation.
Create a Problem Set First
Before starting, make sure you’ve already created a problem set. If not, please create a problem set first by following the previous tutorial.
Add a new problem¶
-
From the problem set editor page, press the plus button in the top right corner.
-
A modal will appear. Select the New problem button in the middle.
-
Give the problem a name (for this example, we’ll call it "Calculator Function") and click Create and add.
Create the problem prompt¶
-
Back on the main screen, a new problem editor will appear.
-
In the problem prompt section, type exactly what you want your students to do. Be as clear as possible since this will be used by the AI to provide assistance to students.
Markdown
The prompt supports both HTML and Markdown formatting. You’ll see a live rendering of the prompt in the right-hand box. For more on Markdown, check out this guide.
Add a Unit test¶
-
Scroll down to the bottom of the problem editor and toggle the button labeled Programmatic.
-
Keep the box labeled Add an example to the new test checked, as we’ll start with this.
-
Click Create a Programmatic test.
The starter example code tests an add()
function in Python using the standard unittest framework. However, all Python unit tests in Grader Than Feedback are executed using pytest, which is compatible with Python's standard unittest
library. This means you can access the full capabilities of both pytest and the unittest
library. For more details, refer to the pytest documentation or the Python standard library documentation.
import unittest
class AddTestCase(unittest.TestCase): # (1)!
def test_add(self):
try: # (2)!
from submitted import add
except ImportError:
self.fail("The function 'add()' is missing!")
result = add(3, 5) # (3)!
self.assertEqual(result, 8) # (4)!
if __name__ == '__main__':
unittest.main()
- This creates a test class. In unit testing, tests are grouped in classes.
- Here, we attempt to import the student’s function from the
submitted
module. The Grader Than system will automatically place the student’s code in a file calledsubmitted.py
. We will access theadd
function from this module. If it doesn’t exist, Python will raise an error, and the test will fail gracefully. - This calls the student’s function and saves the output.
- This checks that the function’s output matches the expected value, in this case,
8
.
This code defines a unit test for the add()
function using Python's unittest
framework. It checks if the function add(3, 5)
correctly returns 8
and fails the test if the function is missing or improperly named.
Modify the unit test for the calculator problem:
- Rename
AddTestCase
toCalculatorTestCase
. - Replace
add
withcalculator
. - Call the
calculator
function like this:calculator('+', 3, 5)
.
The final code should look like this. The lines that have been changes have be highlighted:
import unittest
class CalculatorTestCase(unittest.TestCase):
def test_calculator(self):
try:
from submitted import calculator
except ImportError:
self.fail("The function 'calculator()' is missing!")
result = calculator('+', 3, 5)
self.assertEqual(result, 8)
def test_subtraction(self):
try:
from submitted import calculator
except ImportError:
self.fail("The function 'calculator()' is missing!")
result = calculator('-', 10, 5)
self.assertEqual(result, 5)
def test_multiplication(self):
try:
from submitted import calculator
except ImportError:
self.fail("The function 'calculator()' is missing!")
result = calculator('*', 4, 5)
self.assertEqual(result, 20)
def test_division(self):
try:
from submitted import calculator
except ImportError:
self.fail("The function 'calculator()' is missing!")
result = calculator('/', 20, 5)
self.assertEqual(result, 4)
if __name__ == '__main__':
unittest.main()
Create separate tests for each operator:
Instead of testing all operations in one method, we will create individual test methods for each operator: addition, subtraction, multiplication, and division. Each test method will focus on one specific operation and use Assertions.assertEquals()
to compare the output of the function with the expected result.
The starter example code tests an add()
function in Java using the JUnit framework, which is the standard testing framework for all Java unit tests in Grader Than Feedback. For more details on how to write and run tests with JUnit, refer to the JUnit documentation.
// Import necessary junit dependencies
import org.junit.jupiter.api.Assertions; // (1)!
import org.junit.jupiter.api.Test;
// Import the student's Calculator class
import Calculator; // (2)!
public class CalculatorTest { // (3)!
// A single unit test. You may add more!
@Test
public void testAdd() { // (4)!
Calculator calculator = new Calculator(); // (5)!
int result = calculator.add(5, 3); // (6)!
Assertions.assertEquals(8, result); // (7)!
}
}
-
These lines import the JUnit dependencies needed for testing. JUnit is the framework we use to write unit tests in Java.
-
This imports the student's
Calculator
class from their submitted code. In the background, the Grader Than system places the student's code in a file that can be accessed in this unit test. -
This creates a test class called
CalculatorTest
. In JUnit, test cases are grouped into classes like this. -
The
@Test
annotation marks the method as a unit test. In this case, we’re testing theadd()
method from the student'sCalculator
class. -
This line creates an instance of the
Calculator
class, which will allow us to call theadd()
method. -
Here, we call the
add()
method on thecalculator
object, passing in the arguments5
and3
. The result is stored in theresult
variable. -
Finally, we use
Assertions.assertEquals()
to check if the result is equal to8
. If the result is correct, the test passes. If not, the test fails.
You must tell the student the class name you expect
In your problem prompt, you need to tell the student the name of the class you expect to find in your unit test. In this case, we expect the class to be named Calculator
. This is critical for the code to compile.
Modifying the Unit Test for the Calculator Problem
-
Replace the method call
Change the method call from
add()
tocalculate()
, since the task requires testing acalculate()
function that accepts an operator (+
,-
,*
,/
) and two integers. -
Update the method signature
Adjust the
calculate()
method to handle three inputs: the operator (as achar
), and two integer values. -
Create separate tests for each operator
Instead of testing all operations in one method, we will create individual test methods for each operator: addition, subtraction, multiplication, and division. Each test method will focus on one specific operation and use
Assertions.assertEquals()
to compare the output of the function with the expected result.
Here is the final version of the unit test with separate tests for each operator. The lines that have been changes have be highlighted::
// Import necessary junit dependencies
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
// Import the student's Calculator class
import Calculator;
public class CalculatorTest {
// Test addition
@Test
public void testAdd() {
// Create an instance of the Calculator class
Calculator calculator = new Calculator();
// Test addition
int result = calculator.calculate('+', 3, 5);
Assertions.assertEquals(8, result); // Check if the result is 8
}
// Test subtraction
@Test
public void testSubtract() {
// Create an instance of the Calculator class
Calculator calculator = new Calculator();
// Test subtraction
int result = calculator.calculate('-', 10, 5);
Assertions.assertEquals(5, result); // Check if the result is 5
}
// Test multiplication
@Test
public void testMultiply() {
// Create an instance of the Calculator class
Calculator calculator = new Calculator();
// Test multiplication
int result = calculator.calculate('*', 4, 5);
Assertions.assertEquals(20, result); // Check if the result is 20
}
// Test division
@Test
public void testDivide() {
// Create an instance of the Calculator class
Calculator calculator = new Calculator();
// Test division
int result = calculator.calculate('/', 20, 5);
Assertions.assertEquals(4, result); // Check if the result is 4
}
}
Explanation of Changes
- Separate Tests for Each Operator
Each operator (
+
,-
,*
,/
) has its own dedicated test method:testAdd()
,testSubtract()
,testMultiply()
, andtestDivide()
. This approach makes it easier to isolate and test each operation individually.
By separating the tests, students can earn partial credit for the tests the pass.
The starter example code tests an add()
function in JavaScript using the Jest framework, which is the standard testing framework for all JavaScript unit tests in Grader Than. For more details on how to write and run tests with Jest, refer to the Jest documentation.
const { add } = require('./submitted');
describe('add()', () => {
it('should return the sum of two numbers', () => {
// Test case: Adding 2 and 3 should return 5
const result = add(2, 3);
expect(result).toBe(5);
});
it('should return zero when both numbers are zero', () => {
// Test case: Adding 0 and 0 should return 0
const result = add(0, 0);
expect(result).toBe(0);
});
it('should handle negative numbers correctly', () => {
// Test case: Adding -5 and 10 should return 5
const result = add(-5, 10);
expect(result).toBe(5);
});
});
This code tests the add()
function by passing in different numbers and verifying that the correct result is returned. However, for the calculator problem, we need to modify the test to account for different operations like addition, subtraction, multiplication, and division.
Changes to Make:
-
Replace
add()
withcalculate()
:
The task is to create acalculate()
function that accepts three inputs: the operator (+
,-
,*
,/
), and two integers. We need to change all references toadd()
tocalculate()
. -
Separate Tests for Each Operation:
Instead of testing all operations in one test, we will create separate tests for each operation (+
,-
,*
,/
). This not only makes it easier to organize the tests but also allows students to earn partial credit for correctly implementing individual operations.
Modified Unit Test Example
Here’s how the updated test should look for the calculator problem:
const { calculate } = require('./submitted');
describe('calculate()', () => {
// Test addition
it('should return the sum of two numbers when using "+"', () => {
const result = calculate('+', 3, 5);
expect(result).toBe(8); // 3 + 5 = 8
});
// Test subtraction
it('should return the difference of two numbers when using "-"', () => {
const result = calculate('-', 10, 5);
expect(result).toBe(5); // 10 - 5 = 5
});
// Test multiplication
it('should return the product of two numbers when using "*"', () => {
const result = calculate('*', 4, 5);
expect(result).toBe(20); // 4 * 5 = 20
});
// Test division
it('should return the quotient of two numbers when using "/"', () => {
const result = calculate('/', 20, 5);
expect(result).toBe(4); // 20 / 5 = 4
});
});
Explanation of the Changes:
-
Changing the Function Name:
We replacedadd()
withcalculate()
since the students are required to implement a calculator function that can handle multiple operations, not just addition. Thecalculate()
function accepts three parameters: an operator (+
,-
,*
,/
) and two numbers. -
Creating Separate Tests for Each Operator:
Each mathematical operation is tested in its ownit()
block. This structure makes the code more modular and easier to debug. If one test fails (e.g., subtraction), the other tests (e.g., addition or multiplication) can still pass, allowing students to earn partial credit for the sections they have implemented correctly.
The starter example code tests an add()
function in C++ using the GoogleTest framework, which is the standard testing framework for all C++ unit tests in Grader Than Feedback. For more details on how to write and run tests with GoogleTest, refer to the GoogleTest documentation.
#include <gtest/gtest.h>
// NOTE: Don't worry about including a header file or a main() function
// that will be added automatically!
// Test case
TEST(AddTest, SumOfTwoIntegers) {
// Test inputs
int a = 2;
int b = 3;
// Expected result
int expectedSum = 5;
// Call the function being tested
int actualSum = add(a, b);
// Assertion
ASSERT_EQ(actualSum, expectedSum);
}
This test is designed to check if the add()
function correctly returns the sum of two integers. However, for the calculator problem, we need to test a calculate()
function that handles different operations: addition, subtraction, multiplication, and division.
Steps to Modify the Code
-
Replace the
add()
function withcalculate()
:
Since the task requires students to implement a calculator function, all calls toadd()
must be replaced withcalculate()
. The new function will accept three arguments: an operator (+
,-
,*
,/
), and two integers. -
Create Separate Tests for Each Operation:
We will separate the tests into individual test cases for addition, subtraction, multiplication, and division. This ensures each operation is evaluated independently, which allows for partial credit if a student correctly implements one operation but not the others. -
Partial Credit Explanation:
By creating individual tests for each mathematical operation, students can earn points for completing specific parts of the task. If, for example, a student only completes the addition and multiplication portions of the calculator, they will still receive partial credit for their progress.
Modified Unit Test Example
Here is the updated code with separate test cases for each operator:
#include <gtest/gtest.h>
// NOTE: Don't worry about including a header file or a main() function
// that will be added automatically!
// Test case for addition
TEST(CalculatorTest, Addition) {
// Test inputs
int a = 3;
int b = 5;
// Expected result
int expected = 8;
// Call the function being tested
int actual = calculate('+', a, b);
// Assertion
ASSERT_EQ(actual, expected);
}
// Test case for subtraction
TEST(CalculatorTest, Subtraction) {
// Test inputs
int a = 10;
int b = 5;
// Expected result
int expected = 5;
// Call the function being tested
int actual = calculate('-', a, b);
// Assertion
ASSERT_EQ(actual, expected);
}
// Test case for multiplication
TEST(CalculatorTest, Multiplication) {
// Test inputs
int a = 4;
int b = 5;
// Expected result
int expected = 20;
// Call the function being tested
int actual = calculate('*', a, b);
// Assertion
ASSERT_EQ(actual, expected);
}
// Test case for division
TEST(CalculatorTest, Division) {
// Test inputs
int a = 20;
int b = 5;
// Expected result
int expected = 4;
// Call the function being tested
int actual = calculate('/', a, b);
// Assertion
ASSERT_EQ(actual, expected);
}
Explanation of the Changes
-
Function Name Change:
We replaced all instances ofadd()
withcalculate()
, reflecting the function students are required to implement. This function accepts three arguments: an operator (+
,-
,*
,/
) and two integers. -
Separate Tests for Each Operator:
Instead of testing all operations in a single test case, we created a separate test for each operation:Addition
,Subtraction
,Multiplication
, andDivision
. This way, each test focuses on one specific mathematical operation. -
Assertion Using
ASSERT_EQ()
:
Each test usesASSERT_EQ()
to compare the actual result returned by thecalculate()
function with the expected result. For example, in the addition test, we check whethercalculate('+', 3, 5)
returns8
. -
Partial Credit:
By separating the operations into their own test cases, students can earn partial credit if they correctly implement some of the operations but not all. For example, if the addition and subtraction tests pass but the multiplication and division tests fail, students still receive credit for what they completed successfully.