Introduction
Table-Driven Testing, also known as Data-Driven Testing and Parameterized Test, is a popular methodology in Golang.
The idea is to run several sets of input data against the same logic, in order to increase test coverage. π‘
Let's look at an example in Typescript.
Example
The unit test:
describe('temperature converter', () => {
it.each`
celcius | fahrenheit
${-100} | ${-148}
${0} | ${32}
${100} | ${212}
`(
'converts $celcius degrees C to $fahrenheit degrees F',
({ celcius, fahrenheit }: { celcius: number; fahrenheit: number }) => {
const result = convertCToF(celcius);
expect(result).toBe(fahrenheit);
}
);
});
Explanation
The test utilises Tagged Templates to run several sets of arguments against the same function.
If you're using VS Code, you can install Prettier to format the table for you. π
Benefits of Table-Driven Testing
1. Reduce repetition
Table-Driven Testing not only reduces LOC in unit tests, but it also saves us from writing test descriptions for each of the test cases.
2. Enhance visibility
Table-Driven Testing groups the data at the top, enabling us to compare against the acceptance criteria. It also helps us to identify edge-case scenarios. π
3. Enable integration with real-life data
It's possible to supply an array of data to it.each
. It means we could potentially create an automated system that commit sample production data as a json file to the repository for unit testing purpose.
Below is an example of it.each
using an array as input:
describe('temperature converter', () => {
const testCases = [
{ celcius: -100, fahrenheit: -148 },
{ celcius: 0, fahrenheit: 32 },
{ celcius: 100, fahrenheit: 212 },
];
it.each(testCases)(
'converts $celcius degrees C to $fahrenheit degrees F',
({ celcius, fahrenheit }: { celcius: number; fahrenheit: number }) => {
const result = convertCToF(celcius);
expect(result).toBe(fahrenheit);
}
);
});
Limitations of Table-Driven Testing
1. Harder to debug
When one of the test cases failed in the table, we would need to comment out other test cases to debug.
2. Not as descriptive
A test case should be validating one particular scenario, e.g. positive value, negative value etc.
Without a meaningful description, it can be hard to understand the purpose of each test case.
To address that, we can add a text description column.
3. The table becomes too big
When a table has 4 columns of input and 1 column of output, it can become harder to understand the behaviour by purely reviewing the unit test. π΅
In that case, consider using smaller tables to describe individual behaviours, or refactor the function to receive less inputs.
A Bad Example
When we start using Table-Driven Testing, it's tempting to put every possible outcome into one big table.
And worse still, we may want to put conditionals into the test script. β
Consider the test below:
describe('temperature converter', () => {
it.each`
celcius | fahrenheit | error
${-300} | ${null} | ${'celcius cannot be below 273.15'}
${-100} | ${-148} | ${null}
${0} | ${32} | ${null}
${100} | ${212} | ${null}
`(
'converts $celcius degrees C to $fahrenheit degrees F or throws error $error',
({ celcius, fahrenheit, error }: { celcius: number; fahrenheit?: number; error?: string }) => {
if (!!error) {
expect(() => {
convertCToF(celcius);
}).toThrow(error);
} else {
const result = convertCToF(celcius);
expect(result).toBe(fahrenheit);
}
}
);
});
It has a few issues:
- The table has holes. It means we have to consider
null
when writing the test. - if we need to debug a test case, we would need to determine which code path it takes within the unit test first.
- The test script becomes much harder to read.
The better solution is to write a separate test for throws error if celcius is below 273.15
.
Conclusion
Table-Driven Testing is a useful tool to increase test coverage. If used wisely, it can improve readability as well.
Thank you for reading.
Hope you find this post interesting!
Have you used table-driven testing before?
Share your experience in the comments!
Top comments (0)