This article was first published on my personal blog: teobler.com
Due to the limited space, the article does not provide all the code of the demo. If you are interested, you can clone the code to the local and see the TDD process of the whole demo from the commit. It will be more clear with the article. Teobler/TDD-with-react-rex-demo
Front-end TDD pain
From the understanding of TDD before entering the company to the practice of TDD, a frequent problem I encountered or discussed with my friends in the process is that the front-end is not good TDD/the input and income ratio of front-end TDD is not high. Why is that?
Let’s assume that you’re TDD all the way to the front end, and all you need to do is — first there’s a button on the assert page, then implement the button, then what happens when the Assert clicks on the button, and then implement the logic.
One problem with this process is that the UI and logic are strongly coupled in the front end, so in TDD you need to implement the UI first, and then select the components on the UI to trigger the corresponding behavior, which adds a lot of burden to the developer.
Of course, the code that was written in this way strictly followed TDD and got all the benefits of TDD, but as far as I could see, none of my friends agreed with this approach. Everyone’s pain point is that TDD in UI is too painful and the benefits are too low. Besides, due to the strong coupling between UI and logic, the subsequent logic part also needs to select the element trigger on the page to generate the corresponding execution logic.
These pain points have improved significantly since the project introduced hooks, and it has been nearly a year since the group introduced hooks and has developed a testing strategy together. React components are divided into three categories — pure logic components (such as request processing components, utils functions, etc.), pure UI components (such as Layout for presentation, Container components, etc.) and hybrid components (such as a page).
Pure logical component
15.Tasking this component has nothing to say, all were logic, tasking, testing, implementation, refactoring coordinated, specific how to write we do not discuss here.
// combineClass.test.ts
describe('combineClass'.(a)= > {
it('should return prefixed string given only one class name'.(a)= > {
const result = combineClass('class-one');
expect(result).toEqual('prefix-class-one');
});
it('should trim space for class name'.(a)= > {
const result = combineClass('class-one ');
expect(result).toEqual('prefix-class-one');
});
it('should combine two class name and second class name should not add prefix'.(a)= > {
const result = combineClass('class-one'.'class-two');
expect(result).toEqual('prefix-class-one class-two');
});
it('should combine three class name and tail class name should not add prefix'.(a)= > {
const result = combineClass('class-one'.'class-two'.'class-three');
expect(result).toEqual('prefix-class-one class-two class-three');
});
});
// combineClass.ts
const CLASS_PREFIX = "prefix-";
export const combineClass = (. className:string[]) = > {
const resultName = className.slice(0);
resultName[0] = CLASS_PREFIX + className[0];
return resultName
.join(' ')
.trim();
};
Copy the code
Pure UI components
We did not test the components one by one, but added a Jest JSON snapshot test after building the components according to UX requirements.
Jest generates a DOM structure of the UI using JSON after the component is rendered. In the next test, a new snapshot is generated and compared with the old snapshot, so as to find the differences between the two UIs and achieve UI protection.
However, there are two problems with snapshot testing:
- Snapshot runs slowly compared to general unit tests. If snapshot tests are used extensively in a project, the speed of unit tests will be significantly slowed down when all unit tests are run, which to some extent violates the original intention of fast feedback of unit tests.
- The biggest problem with snapshot testing is that if you change any part of the UI, the test will fail. In this case, you need to carefully compare the different places to determine whether you are updating snapshot or changing the wrong places. If there were a “spare” team member updating snapshot mindlessly, the test would be a waste of resources.
// Content.test.tsx describe('Content', () => { it('should render correctly', () => { const {container} = render(<Content/>); expect(container).toMatchSnapshot(); }); }); // Content.test.tsx.snap // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Content should render correctly 1`] = ` <div> <main class="prefix-layout-content" /> </div> `; // Content.tsx export const Content: React.FC<React.HTMLAttributes<HTMLElement>> = (props) => { const { className = '', children, ... restProps } = props; return ( <main className={combineClass('layout-content', className)} {... restProps}> {children} </main> ); };Copy the code
Logic and UI mix components
This is a component that needs hooks that are strongly coupled to the UI and logic, so we can separate them. So for such a component we would write:
-
Build the UI page first, but write all the required callback as empty functions
-
Pull all the callback or logic needed for the page into a hook
-
At this time, the code in hook has no UI and only logic, so the test library can be used to conduct a separate logic test on hook, so the development of hook can be TDD according to the development of logical components
-
After the whole mixed component is developed, a Snapshot test is added. Note that the component may require some data during rendering. Ensure that the data prepared is complete when writing the Snapshot test, otherwise the snapshot will render an incorrect component with no data at all
// usePageExample.test.ts import {act, renderHook} from "@testing-library/react-hooks"; describe('usePageExample', () => { let mockGetUserId: jest.Mock; let mockValidate: jest.Mock; beforeAll(() => { mockGetUserId = jest.fn(); mockValidate = jest.fn(); jest.mock('.. /.. /.. /.. /request/someRequest', () => ({ getUserId: mockGetUserId, })); jest.mock('.. /.. /.. /.. /validator/formValidator', () => ({ formValidate: mockValidate, })); }); afterAll(() => { mockGetUserId.mockReset(); mockValidate.mockReset(); }); it('should trigger request with test string when click button', () => { const {usePageExample} = require('.. /usePageExample'); const {result} = renderHook(() => usePageExample()); act(() => { result.current.onClick(); }); expect(mockGetUserId).toBeCalled(); }); it('should validate form values before submit', () => { const {usePageExample} = require('.. /usePageExample'); const {result} = renderHook(() => usePageExample()); const formValues = {id: '1', name: 'name'}; act(() => { result.current.onSubmit(formValues); }); expect(mockValidate).toBeCalledWith(formValues); }); }); // usePageExample.ts import {getUserId} from ".. /.. /.. /request/someRequest"; import {formValidate} from ".. /.. /.. /validator/formValidator"; export interface IFormValues { email: string; name: string; } export const usePageExample = () => { const onClick = () => { getUserId(); }; const onSubmit = (formValues: IFormValues) => { formValidate(formValues); }; return {onClick, onSubmit}; }; // PageExample.tsx import * as React from "react"; import {usePageExample} from "./hooks/usePageExample"; export const PageExample: React.FC<IPageExampleProps> = () => { const {onClick, onSubmit} = usePageExample(); return ( <div> <form onSubmit={() => onSubmit}> <input type="text"/> </form> <button onClick={onClick}>test</button> </div> ); };Copy the code
This article provides an hooks approach to TDD, of course there are a few areas that we don’t think are perfect (such as UI testing). If you have better practices, please discuss them.