Writing Integration Tests
Installing dependencies
Creating an Integration Test Suite File
We will create a file named viewGitHubRepositoriesByUsername.spec.js file in the ./test folder of our application. Jest will automatically pick it up.
Importing Dependencies in the Test File
import Reactfrom 'react';// so that we can use JSX syntaximport {
render,
cleanup,
waitForElement
}from '@testing-library/react';// testing helpers
import userEventfrom '@testing-library/user-event'// testing helpers for imitating user events
import 'jest-dom/extend-expect';// to extend Jest's expect with DOM assertions
import nockfrom 'nock';// to mock github API
import {
FAKE_USERNAME_WITH_REPOS,
FAKE_USERNAME_WITHOUT_REPOS,
FAKE_BAD_USERNAME,
REPOS_LIST
} from './fixtures/github';// test data to use in a mock API
import './helpers/initTestLocalization';// to configure i18n for tests
import Appfrom '../App';// the app that we are going to test
TypeScript
복사
Setting up The Test Suite
describe('view GitHub repositories by username', () => {
beforeAll(() => {
nock('https://api.github.com')
.persist()
.get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`)
.query(true)
.reply(200, REPOS_LIST);
});
afterEach(cleanup);
describe('when GitHub user has public repositories', () => {
it('user can view the list of public repositories for entered GitHub username',async () => {
// arrange
// act
// assert
});
});
describe('when GitHub user has no public repositories', () => {
it('user is presented with a message that there are no public repositories for entered GitHub username',async () => {
// arrange
// act
// assert
});
});
describe('when GitHub user does not exist', () => {
it('user is presented with an error message',async () => {
// arrange
// act
// assert
});
});
});
TypeScript
복사
Notes:
•
Prior to all tests, mock the GitHub API to return a list of repositories when called with a specific username.
모든 테스트를 수행하기 전에 GitHub API를 mocking하여 특정 사용자 이름으로 호출될 때 리포지토리 목록을 반환합니다.
•
After each test, clean the test React DOM so that each test starts from a clean spot.
각각의 테스트가 끝난 후, 각각의 테스트가 clean spot에서 시작되도록 각각의 test React DOM을 청소한다.
•
describe blocks specify the integration test use case and the flow variations.
describe 블록은 통합 테스트 유스케이스 및 flow variations을 지정합니다.
•
The flow variations we are testing are:
◦
User enters a valid username that has associated public GitHub repositories.
사용자가 공용 GitHub 레포를 연결한 올바른 사용자 이름을 입력합니다.
◦
User enters a valid username that has no associated public GitHub repositories.
사용자가 연결된 공용 GitHub 레가포 없는 올바른 사용자 이름을 입력하십시오.
◦
User enters a username that does not exist on GitHub.
사용자가 GitHub에 존재하지 않는 사용자 이름을 입력합니다.
•
it blocks use async callback as the use case they are testing has asynchronous step in it.
테스트 중인 유스케이스에 비동기 단계가 포함되어 있으므로 비동기 콜백을 사용합니다.
Writing the First Flow Test
First, the app needs to be rendered.
먼저 앱을 렌더링해야 합니다.
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
TypeScript
복사
The render method imported from the @testing-library/react module renders the app in the test React DOM and returns DOM queries bound to the rendered app container. These queries are used to locate DOM elements to interact with and to assert on.
@testing-library/react 모듈에서 가져온 render 메서드는 test React DOM에서 앱을 렌더링하고 렌더링된 앱 컨테이너에 바인딩된 DOM 쿼리를 반환합니다. 이러한 쿼리는 상호 작용하고 어설션할 DOM 요소를 찾는 데 사용됩니다.
Now, as the first step of the flow under test, the user is presented with a username field and types a username string into it.
이제 테스트 중인 흐름의 첫 번째 단계로 사용자에게 사용자 이름 필드가 표시되고 사용자 이름 문자열을 입력합니다.
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
TypeScript
복사
The userEvent helper from imported @testing-library/user-event module has a type method that imitates the behavior of the user when they type text into a text field. It accepts two parameters: the DOM element that accepts the input and the string that the user types.
가져온 @testing-library/user-event 모듈의 userEvent helper에는 텍스트를 텍스트 필드에 입력할 때 사용자의 동작을 모방하는 type 메서드가 있습니다.
입력을 받아들이는 DOM 요소와 사용자가 입력하는 문자열의 두 가지 매개 변수를 허용합니다.
Users usually find DOM elements by the text associated with them. In the case of input, it is either label text or placeholder text. The getByPlaceholderText query method returned earlier from render allows us to find the DOM element by placeholder text.
사용자는 일반적으로 DOM 요소와 관련된 텍스트로 DOM 요소를 찾습니다. 입력의 경우 label 텍스트 또는 placeholder 텍스트입니다. render에서 이전에 반환된 getByPlaceholderText 쿼리 메서드를 사용하여 placeholder 텍스트로 DOM 요소를 찾을 수 있습니다.
Please note that since the text itself is often likely to change, it is best to not rely on actual localization values and instead configure the localization module to return a localization item key as its value.
텍스트 자체는 자주 변경될 수 있으므로 실제 localization 값에 의존하지 않고 localization 항목 키를 값으로 반환하도록 지역화 모듈을 구성하는 것이 가장 좋습니다.
For example, when “en-US” localization would normally return Enter GitHub username as the value for the userSelection.usernamePlaceholder key, in tests, we want it to return userSelection.usernamePlaceholder.
예를 들어, "en-US" localization가 일반적으로 테스트에서 userSelection.usernamePlaceholder 키에 대한 값으로 Enter GitHub username을 반환하는 경우 userSelection.usernamePlaceholder를 반환하려고 합니다.
When the user types text into a field, they should see the text field value updated.
사용자가 필드에 텍스트를 입력할 때 텍스트 필드 값이 업데이트된 것을 확인해야 합니다.
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
TypeScript
복사
Next in the flow, the user clicks on the submit button and expects to see the list of repositories.
다음 흐름에서 사용자는 제출 단추를 클릭하고 리포지토리 목록을 볼 수 있습니다.
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
TypeScript
복사
The userEvent.click method imitates the user clicking on a DOM element, while getByText query finds a DOM element by the text it contains. The closest modifier ensures that we select the element of the right kind.
userEvent.click은 사용자가 DOM 요소를 클릭하는 것을 모방합니다.
반면 getByText 쿼리는 포함된 텍스트로 DOM 요소를 찾습니다. closest modifier는 우리가 적절한 종류의 원소를 선택하도록 보장한다.
Note: In integration tests, steps often serve both act and assert roles. For example, we assert that the user can click a button by clicking it.
단계는 종종 'act' 및 'assport' 역할을 모두 수행합니다. 예를 들어, 우리는 사용자가 버튼을 클릭하여 클릭할 수 있다고 주장한다.*
In the previous step, we asserted that the user sees the repositories list section of the app. Now, we need to assert that since fetching the list of repositories from GitHub may take some time, the user sees an indication that the fetching is in progress. We also want to make sure that the app does not tell the user that there are no repositories associated with the entered username while the repositories list is still being fetched.
이전 단계에서, 우리는 앱의 리포지토리 목록 섹션을 사용자가 본다고 주장했습니다. 이제 GitHub에서 저장소 목록을 가져오는 데 시간이 걸릴 수 있으므로 사용자는 가져오기가 진행 중이라는 표시를 볼 수 있다고 주장해야 합니다. 또한 리포지토리 목록을 가져오는 동안 앱이 사용자에게 입력한 사용자 이름과 연결된 리포지토리가 없음을 알리지 않도록 해야 합니다.
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
TypeScript
복사
Note that the getBy query prefix is used to assert that the DOM element can be found and the queryBy query prefix is useful for the opposite assertion. Also, queryBy does not return an error if no element is found.
getBy 쿼리 접두사는 DOM 요소를 찾을 수 있다고 주장하는 데 사용되며 queryBy 접두사는 반대쪽 어설션에서 유용합니다. 또한 요소를 찾을 수 없는 경우 queryBy는 오류를 반환하지 않습니다.
Next, we want to make sure that, eventually, the app finishes fetching repositories and displays them to the user.
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => {
elementsToWaitFor.push(getByText(repository.name));
elementsToWaitFor.push(getByText(repository.description));
return elementsToWaitFor;
}, []));
TypeScript
복사
The waitForElement asynchronous method is used to wait for a DOM update that will render the assertion provided as method parameter true. In this case, we assert that the app displays the name and description for every repository returned by the mocked GitHub API.
waitForElement 비동기 메서드는 메서드 매개 변수로 제공된 어설션을 true로 렌더링할 DOM 업데이트를 기다리는 데 사용됩니다. 이 경우, 우리는 앱이 mocking된 GitHub API에 의해 반환된 모든 리포지토리의 이름과 설명을 표시한다고 주장한다.
Finally, the app should no longer display an indicator that repositories are being fetched and it should not display an error message.
마지막으로, 앱은 더 이상 리포지토리를 가져오고 있다는 표시기와 오류 메시지를 표시하지 않아야 합니다.
expect(queryByText('repositories.loadingText')).toBeNull();
expect(queryByText('repositories.error')).toBeNull();
TypeScript
복사
Our resulting React integration test looks like this:
it('user can view the list of public repositories for entered GitHub username',
async () => {
// Arrange
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
// Act
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
// Assert
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
// Act
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
getByText('repositories.loadingText');
// Assert
expect(queryByText('repositories.empty')).toBeNull();
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => {
elementsToWaitFor.push(getByText(repository.name));
elementsToWaitFor.push(getByText(repository.description));
return elementsToWaitFor;
}, []));
expect(queryByText('repositories.loadingText')).toBeNull();
expect(queryByText('repositories.error')).toBeNull();
});
TypeScript
복사
Alternate Flow Tests
When the user enters a GitHub username with no associated public repositories, the app displays an appropriate message.
사용자가 연결된 공용 리포지토리가 없는 GitHub 사용자 이름을 입력하면 앱에 해당 메시지가 표시됩니다.
describe('when GitHub user has no public repositories', () => {
it('user is presented with a message that there are no public repositories for entered GitHub username',async () => {
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS);
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
await waitForElement(() => getByText('repositories.empty'));
expect(queryByText('repositories.error')).toBeNull();
});
});
TypeScript
복사
When user enters a GitHub username that does not exist, the app displays an error message.
사용자가 존재하지 않는 GitHub 사용자 이름을 입력하면 앱에 오류 메시지가 표시됩니다.
describe('when GitHub user does not exist', () => {
it('user is presented with an error message',async () => {
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME);
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
await waitForElement(() => getByText('repositories.error'));
expect(queryByText('repositories.empty')).toBeNull();
});
});
TypeScript
복사
Why React Integration Tests Rock
통합 테스트가 중요한 이유
Integration testing truly offers a sweet spot for React applications. These tests help catch bugs and use the TDD approach while, at the same time, they do not require maintenance when implementation changes.
통합 테스트는 Respect 애플리케이션에 매우 유용합니다. 이러한 테스트는 버그를 발견하고 TDD 접근 방식을 사용하는 동시에 구현 변경 시 유지보수가 필요하지 않습니다.
React-testing-library, showcased in this article, is a great tool for writing React integration tests, as it allows you to interact with the app as the user does and validate app state and behavior from the user’s perspective.
이 글에서 설명하는 리액트 테스팅 라이브러리는 사용자가 하는 것처럼 앱과 상호 작용하고 사용자의 관점에서 앱 상태와 동작을 검증할 수 있으므로 리액트 통합 테스트를 작성하기 위한 훌륭한 도구입니다.
Hopefully, the examples provided here will help you start writing integration tests on new and existing React projects. The full sample code that includes the app implementation can be found at my GitHub.
여기에 제공된 예제가 신규 및 기존 리액트 프로젝트에 대한 통합 테스트를 시작하는 데 도움이 되기를 바랍니다. 앱 구현이 포함된 전체 샘플 코드는 내 GitHub에서 확인할 수 있습니다.