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
Top comments (2)
Great Article @sakethkowtha
Thank you @deepanshushukla