Mastering Jest `test.each` in TypeScript

In the world of software development, testing is a crucial part of the process to ensure the reliability and correctness of our code. Jest, a popular JavaScript testing framework, provides several useful features to simplify the testing process. One such powerful feature is test.each, which allows you to write parameterized tests. When combined with TypeScript, test.each becomes even more robust as it brings in the benefits of static typing. This blog post will explore the fundamental concepts of using test.each in TypeScript, its usage methods, common practices, and best practices.

Table of Contents#

  1. Fundamental Concepts
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts#

What is test.each?#

test.each is a method provided by Jest that allows you to run the same test with multiple sets of data. It is a form of parameterized testing, which means you can define a single test case and then provide different input values and expected outputs. This is especially useful when you have a function that should behave the same way for different sets of inputs.

Why use test.each in TypeScript?#

TypeScript adds static typing to JavaScript, which helps catch errors at compile-time rather than runtime. When using test.each in TypeScript, you can define the types of your input and output values, making your tests more robust and easier to understand. It also provides better autocompletion and documentation within your IDE.

Usage Methods#

Basic Syntax#

The basic syntax of test.each in Jest with TypeScript is as follows:

test.each([
  [input1, expectedOutput1],
  [input2, expectedOutput2],
  // ... more test cases
])('description for the test with %p', (input, expectedOutput) => {
  // Test logic here
  const result = yourFunction(input);
  expect(result).toBe(expectedOutput);
});

Here, %p is a placeholder for the input value, which will be replaced with the actual input value when the test is run.

Example#

Let's consider a simple function that adds two numbers:

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

Now, let's write tests for this function using test.each:

// math.test.ts
import { add } from './math';
 
test.each([
  [1, 2, 3],
  [0, 0, 0],
  [-1, 1, 0]
])('add(%p, %p) should return %p', (a, b, expected) => {
  const result = add(a, b);
  expect(result).toBe(expected);
});

In this example, we are testing the add function with three different sets of input values and their corresponding expected outputs.

Common Practices#

You can group related tests using describe blocks. This helps in organizing your tests and making them more readable.

// math.test.ts
import { add } from './math';
 
describe('add function', () => {
  test.each([
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0]
  ])('add(%p, %p) should return %p', (a, b, expected) => {
    const result = add(a, b);
    expect(result).toBe(expected);
  });
});

Testing Different Scenarios#

Use test.each to test different scenarios of a function. For example, if you have a function that validates an email address, you can test valid and invalid email addresses:

// emailValidator.ts
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}
 
// emailValidator.test.ts
import { isValidEmail } from './emailValidator';
 
describe('isValidEmail function', () => {
  test.each([
    ['[email protected]', true],
    ['invalidemail', false],
    ['@example.com', false]
  ])('isValidEmail(%p) should return %p', (email, expected) => {
    const result = isValidEmail(email);
    expect(result).toBe(expected);
  });
});

Best Practices#

Use Descriptive Test Descriptions#

Make sure your test descriptions are descriptive. The description should clearly convey what the test is about. For example, instead of using a generic description like "test 1", use something like "add(1, 2) should return 3".

Type Annotations#

Use TypeScript type annotations for the input and output values in your tests. This makes your tests more self-explanatory and helps catch type-related errors.

test.each<[number, number, number]>([
  [1, 2, 3],
  [0, 0, 0],
  [-1, 1, 0]
])('add(%p, %p) should return %p', (a: number, b: number, expected: number) => {
  const result = add(a, b);
  expect(result).toBe(expected);
});

Keep Test Data Separate#

If you have a large number of test cases, consider keeping the test data separate from the test logic. This makes the test code more maintainable.

// math.test.ts
import { add } from './math';
 
const testData: [number, number, number][] = [
  [1, 2, 3],
  [0, 0, 0],
  [-1, 1, 0]
];
 
describe('add function', () => {
  test.each(testData)('add(%p, %p) should return %p', (a, b, expected) => {
    const result = add(a, b);
    expect(result).toBe(expected);
  });
});

Conclusion#

Jest's test.each is a powerful feature that simplifies the process of writing parameterized tests. When used in conjunction with TypeScript, it becomes even more effective due to the benefits of static typing. By following the common and best practices outlined in this blog post, you can write more robust, readable, and maintainable tests for your TypeScript applications.

References#