Test driven development TDD = Test first development + Refactor
- 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.
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.
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.
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.
To start with TDD in react we need.
- Test framework
- Assertion library
Testing frameworks are used to organise and execute tests.
Example: Jamine, Jest, Mocha
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.
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);
}
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
test & it
describe
beforeEach
afterEach
beforeAll
beforeEach
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.
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", () => {});
});
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
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
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
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...
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;
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;
const add = (a, b) => a + b;
test.todo("should add two numbers");
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
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
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();
});
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();
});
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()
})
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)
})
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
- queryBy
- getBy
- findBy