TDD with RTL and Jest

TDD with RTL and Jest

TDD

Test driven development TDD = Test first development + Refactor

Why TDD

  • TDD will force developer to write a clean code.
  • Your code will be tested before it went to deployment. So the chances of getting errors in production is less.
  • It will actually make you think about requirments before you code.
  • It will also help to find a loopholes in the at the time of development.

TDD will work like this

Step 1: Write a code Step 2: Try to break it Step 3: Refactor the code and fix it Step 4: Repeat again from Step 1. Unit you feel there is nothing left to test.

How TDD will force developer to write a clean code

If the Function / Module or Component is small and it has a single responsibility then it is easy to test. Testing a large component is challenging and testing a component which has async actions is like working in a hell. So if you want to good experience with TDD then you have to design your component small and SOLID.

How TDD help to find bugs before deployment and how will it reduces the chances of errors in prod?

If you look into the TDD process in step 2 you have to break your code. If you are clear with requirements you will write happy path tests and then you will start thinking in negative scenarios. So you can make sure that your code is working fine for happy path and giving proper messages / errors for negative scenarios.

Note: TDD will also gives you a confidence on your code. If you test your code 100% then it won't break in prod. So you can be confident at the time of deployment and release activity.

TDD in React Js

To start with TDD in react we need.

  • Test framework
  • Assertion library
Test framework

Testing frameworks are used to organise and execute tests. Example: Jamine, Jest, Mocha

Assertion library

Assertion libraries are tools to verify that things are correct. Example: Chai, Enzyme, Testing library, Should.js

Note: Jest is a testing framework also it has built-in assertion lib.

Examples for Assertion and Test framework

For Assertion:
var output = mycode.doSomething();
output.should.equal("bacon"); //should.js
assert.eq(output, "bacon"); //node.js assert

// The alternative being:
var output = mycode.doSomething();
if (output !== "bacon") {
  throw new Error('expected output to be "bacon", got ' + output);
}
For Test framework:
describe("mycode.doSomething", function () {
  it("should work", function () {
    var output = mycode.doSomething();
    output.should.equal("bacon");
  });
  it("should fail on an input", function () {
    var output = mycode.doSomething("a input");
    output.should.be.an.Error;
  });
});

Learn more about jest Learn more about react-testing-library

Some useful functions used in jest for writing / setting tests

  • test & it
  • describe
  • beforeEach
  • afterEach
  • beforeAll
  • beforeEach
1. test & it

These 2 functions are same. There is no difference in the functionality. Just it is about readability.

Consider the following example:

describe('Module', () => {
  test('if it does this thing', () => {});
  test('if it does the other thing', () => {});
});

output in CLI:
Module > if it does this thing

describe('Module', () => {
  it('should do this thing', () => {});
  it('should do the other thing', () => {});
});

output in CLI:
yourModule > should do this thing

Note: Choose which one is more readable for you.

2. describe

Describe is used to create a block that groups together several related tests.

describe("Calculator", () => {
  it("should add two numbers", () => {});
  it("should sub two numbers", () => {});
});
3. beforeEach

Runs a function before each of the tests in this file runs. If the function returns a promise or a generator, Jest waits for that promise to resolve before running the test.

describe('Calculator', () => {
    beforeEach(() => {
        console.log('Before executing it')
    })
  it('should add two numbers', () => {
     console.log('Add')
  });
  it('should sub two numbers', () => {
     console.log('Sub')
  });
});
Output:
Before executing it
Add
Before executing it
Sub
4. afterEach

Runs a function after each of the tests in this file runs. If the function returns a promise or a generator, Jest waits for that promise to resolve after running the test.

describe('Calculator', () => {
    afterEach(() => {
        console.log('After executing it')
    })
  it('should add two numbers', () => {
     console.log('Add')
  });
  it('should sub two numbers', () => {
     console.log('Sub')
  });
});
Output:
Add
After executing it
Sub
After executing it
5. beforeAll

Runs a function before all of the tests in this file runs. If the function returns a promise or a generator, Jest waits for that promise to resolve before running all the tests.

describe('Calculator', () => {
    beforeAll(() => {
        console.log('Before executing it')
    })
  it('should add two numbers', () => {
     console.log('Add')
  });
  it('should sub two numbers', () => {
     console.log('Sub')
  });
});
Output:
Before executing it
Add
Sub
6. afterAll

Runs a function after all of the tests in this file runs. If the function returns a promise or is a generator, Jest waits for that promise to resolve after running all the tests.

describe('Calculator', () => {
    afterAll(() => {
        console.log('After executing it')
    })
  it('should add two numbers', () => {
     console.log('Add')
  });
  it('should sub two numbers', () => {
     console.log('Sub')
  });
});
Output:
Add
Sub
After executing it

beforeEach, afterEach, beforeAll, afterAll will be useful to do some setup work or any initialization or creating / clearing mocks etc...

Skipping test or test suite

Use xdescribe(....) or xit(....) or it.skip(....) or describe.skip(....) to skip specific test or test suite.

describe("Calculator", () => {
  it("should add two numbers", () => {
    console.log("Add");
  });
  it.skip("should sub two numbers", () => {
    //Can use other options instead it.skip.
    console.log("Sub");
  });
});
Output: Add;
Runing particular test or test suite

Use fdescribe(....) or fit(....) or it.only(....) or describe.only(....) to run specific test or test suite.

describe("Calculator", () => {
  fit("should add two numbers", () => {
    //Can use other options instead fit.
    console.log("Add");
  });
  it.skip("should sub two numbers", () => {
    console.log("Sub");
  });
});
Output: Add;
If your planning to write a test with out assertion use todo
const add = (a, b) => a + b;

test.todo("should add two numbers");

Mocks

Mock is like overriding the actual implementation of the function with custom logic.

Example :

//Function
const printAdditionOfTwoNumbers = (x, y) => console.log(x + y);

//Test
test("should add two numbers and should print", () => {
  console.log = jest.fn();
  printAdditionOfTwoNumbers(1, 2);
  expect(printAdditionOfTwoNumbers).toBeCalledWith(3);
});

There are many ways to mock and also ways to mock promises. We can mock even only once also we can set resolved / rejected values for mock functions if those are promises.

Note: We can mock modules /exports / named exports / functions / async functions / promises / React components etc...

jest.mock will mock complete module / object. If you are using named exports or any object. Instead of mocking entire object you want mock only for particular method or module in the object then instead mock one can use spyOn.

spyOn will also mock the function but instead if mocking completely. We can just mock required part

test("it should console warn a message", () => {
  jest.spyOn(global.console, "warn").mockImplementation();

  console.warn("my error");
  expect(console.warn).toBeCalledTimes(1);
  expect(console.warn).toBeCalledWith("my error");
});

//Just mocking console.warn. Rest of the console methods will be same

React testing library

It is also referred as RTL

  • RTL is like a wrapper for DOM Testing library with reactjs support
  • It is light weight over other react testing libraries
  • It provides good utility functions those will help us to write tests in best practices
  • Good querying functions

Some examples for using RTL

  • Testing basic stateless react component
  • Testing components which has hooks
  • Testing components which has API call
  • Testing components which has API call and loader
  • Testing custom hooks
  • Testing user events
Testing basic stateless react component
import { render, screen } from "@testing-library/react";

const MyApp = () => {
  return <div>Hello world</div>;
};

test("MyApp should render hello world", () => {
  render(<MyApp />);
  expect(screen.getByText("Hello world")).toBeInTheDocument();
});

Testing component with props

import { render, screen } from "@testing-library/react";

const MyApp = ({ message }) => {
  return <div>{message}</div>;
};

test("MyApp should render hello world", () => {
  render(<MyApp message={"Hello world"} />);
  expect(screen.getByText("Hello world")).toBeInTheDocument();
});
Testing components which has hooks
import { render, screen } from "@testing-library/react";

const MyApp = () => {
  let { id } = useParams();
  return <div>{id}</div>;
};

test("MyApp should render hello world", () => {
  jest.mock("react-router-dom", () => ({
    ...jest.requireActual("react-router-dom"),
    useParams: jest.fn().mockReturnValue({ id: "123" }),
  }));
  render(<MyApp />);
  expect(screen.getByText("123")).toBeInTheDocument();
});
Testing components which has API call
import {getArticles} from './services'
import {render, screen} from '@testing-library/react'

const MyApp = () => {
    const [articles, setArticles] = useState([])
  useEffect(() => {
      const response = await getArticles()
      setArticles(response)
  }, [])
    return <div>
            {
                articles.map(article => <div>{article}</div>)
            }
        </div>
}

test("MyApp should render hello world", () => {
    jest.mock('./services', () => ({
      getArticles: jest.fn()
    }));
    render(<MyApp />)
    expect(getArticles).toBeCalled()
})
Testing components which has API call and loader
import {getArticles} from './services'
import {render, screen} from '@testing-library/react'

const MyApp = () => {
    const [articles, setArticles] = useState([])
    const [showLoader, setShowLoader] = useState(false)

  useEffect(() => {
      setShowLoader(true)
      const response = await getArticles()
      setShowLoader(false)
      setArticles(response)
  }, [])
    if(showLoader) return <Loader data-testId="loader" />
    return <div>
            {
                articles.map((article, index) => <div key={index}>{article}</div>)
            }
        </div>
}

test("MyApp should render hello world", async () => {
    const mockResponse = ["Article 1", "Article 2"]
    jest.mock('./services', () => ({
      getArticles: jest.fn().mockResolveValue(mockResponse)
    }));
    render(<MyApp />)
    const loader = screen.queryByTestId("loader")
  expect(loader).toBeInTheDocument()
    await waitForElementToBeRemoved(() => loader)
    expect(screen.getAllByText("Article").length).toBe(2)
})
Testing user events
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const MyApp = () => {
  const [counter, setCounter] = useState(0);
  return (
    <div>
      <button data-testId="inc-btn" onClick={() => setCounter(counter + 1)}>
        +
      </button>
      <span data-testId="value">{counter}</span>
      <button data-testId="dec-btn" onclick={() => setCounter(counter - 1)}>
        -
      </button>
    </div>
  );
};

test("MyApp should render hello world", () => {
  render(<MyApp />);
  const counterValue = screen.getByTestId("value");
  expect(counterValue).toBe("0");
  userEvent.click(screen.getByTestId("inc-btn"));
  expect(counterValue).toBe("1");
  userEvent.click(screen.getByTestId("dec-btn"));
  expect(counterValue).toBe("0");
});

Note: Testing custom hook is an advanced thing please refer this link for testing a custom hook with RTL

Did you find this article valuable?

Support Saketh Kowtha by becoming a sponsor. Any amount is appreciated!