If I want to test a TypeScript function with lots of varied inputs to ensure good coverage, how do I go about it without copy and pasting a lot of test scaffolding code?
For this post, we want to test a toCamelCase
formatter with lots of different cases to make sure it does what we expect every time.
The initial approach is to copy and paste the test, changing the input and expected result for each case. While this gives us good coverage, there is a lot of repeated code to maintain in the future. This is also asking for a copy-paste error!
describe('Test toCamelCase formatter: ', () => {
beforeEach(() => {
// Test setup code
});
it('No Change', () => {
expect(toCamelCase('test')).toBe('test');
});
it('Single Word Case', () => {
expect(toCamelCase('TeST')).toBe('test');
});
it('Two Words', () => {
expect(toCamelCase('test Me')).toBe('testMe');
});
it('Two Words Case', () => {
expect(toCamelCase('TEST ME')).toBe('testMe');
});
});
To reduce the amount of copied test code, we could put all the inputs and expectations in a single test. This greatly reduces the amount of test code, but we lose visibility on each test case and some cases may not run if a previous check fails.
This also breaks tests that require setup via the beforeEach()
method as it is not run between each test.
describe('Test toCamelCase formatter: ', () => {
beforeEach(() => {
// POTENTIAL BUG: Test setup code not run between each expectation!
});
it('All Tests', () => {
expect(toCamelCase('test')).toBe('test');
expect(toCamelCase('TeST')).toBe('test');
expect(toCamelCase('test Me')).toBe('testMe');
expect(toCamelCase('TEST ME')).toBe('testMe');
});
});
ForEach
your it()
callsWhat we really want is to define a list of test cases and run each through the same test function. This means every test is run independently and we have no copy and pasting of test code!
To achieve this, we first create an array of test cases using the TestCase
interface.
interface TestCase { name: string, value: string, expected: string };
const testCases : TestCase[] = [
{name: 'No Change', value: 'test', expected: 'test'},
{name: 'Single Word Case', value: 'TesT', expected: 'test'},
{name: 'Two Words', value: 'test ME', expected: 'testMe'},
{name: 'Two Words Case', value: 'TEST ME', expected: 'testMe'},
];
Then, we define our test it()
function within a foreach
loop of the testCases
array.
describe('Test toCamelCase formatter: ', () => {
beforeEach(() => {
// Test setup code
});
testCases.forEach(tc =>
{
it(tc.name, () => {
expect(toCamelCase(tc.value)).toBe(tc.expected);
});
});
});
Now, every test case is run individually, giving full test coverage, and avoids the beforeEach
bug above. Happy days!
I got this idea from a tweet by WesGrimes so thanks to him for sharing it! In writing this post, I came across a Jasmine plugin called Jasmine Data Driven Tests by Greg Burghardt which looks to make these data-driven tests even shorter to write.
Plugin or no plugin, this little trick has helped me delete a lot of duplicated test code and encouraged me to improve test coverage by simplifying the creation of new test cases.
I would be interested to know if other testing libraries handle this natively. I notice that Jest has an describe.each function, which looks promising.