DEV Community

Cover image for TDD in reactjs with React testing library (RTL) and Jest
sakethk
sakethk

Posted on • Edited on

TDD in reactjs with React testing library (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);
}
Enter fullscreen mode Exit fullscreen mode
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;
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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", () => {});
});
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode
If your planning to write a test with out assertion use todo
const add = (a, b) => a + b;

test.todo("should add two numbers");
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode
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();
});
Enter fullscreen mode Exit fullscreen mode
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()
})
Enter fullscreen mode Exit fullscreen mode
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)
})
Enter fullscreen mode Exit fullscreen mode
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");
});
Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
deepanshushukla profile image
Deepanshu Shukla

Great Article @sakethkowtha

Collapse
 
sakethkowtha profile image
sakethk

Thank you @deepanshushukla