22
loading...
This website collects cookies to deliver better user experience
All the code to support this blog post is available on Github in the React Data Grid Podcast Project, in particular the version 8 folder is the version where Testing Library was used.
create-react-app
.npm test
npm test -- --verbose
create-react-app
creates a default test for each project in App.test.js
, we may find that our tests are broken as soon as we start development because one of the first things we do is change the heading for our application.import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
App.test.js
file after creating their application with the create-react-app
bootstrap.test('renders the app', () => {
render(<App />);
const headerElement = screen.getByText(/Podcast Player/i);
expect(headerElement).toBeInTheDocument();
});
h1
I have a working test which I can then use to learn more about the Testing Library.user-event
which makes it easier to simulate user interaction with the DOM.create-react-app
is used, Jest is configured as the default test runner.create-react-app
looks as follows:test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
test
comes from Jest and allows us to write 'tests' and report on pass/failuresrender
comes from the react wrapper of Testing Library and renders a React component in a virtual DOM, waiting for the initial events to complete.screen
comes from the core Testing Library and is a convenience object which maps to document.body
and has the query methods provided by Testing Library pre-bound e.g. allowing screen.getByText("Podcast")
expect
comes from Jest and allows creating assertions with matchers
toBeInTheDocument
is a matcher from the jest-dom library installed when we used create-react-app
create-react-app
most of the libraries are installed.user-event
and dom
libraries:npm install --save-dev @testing-library/user-event @testing-library/dom
test
in the name, this allows them to be found by Jest when npm test
is run.App.test.js
this contains tests that explore the interaction between form fields and the grid.PodcastGrid.text.js
this contains tests that look at the Grid component in isolation.AgGridReact
directly in my application I wrap it in a component to make it easier to configure with properties and handle the support methods unique to the rendering of podcasts.PodcastGrid
rather than AgGridReact
.getByLabelText
getByRole
getByPlaceholderText
getByText
data-testid
attribute to identify elements on the screendata-testid
attributes into the 3rd party components. As a personal preference I try to avoid adding automation specific attributes into my code, so I would tend to avoid data-testid
for that reason alone, but with a locator strategy built around CSS Selectors it doesn't really matter.querySelector
or querySelectorAll
.querySelector
as a query strategy is that it is vulnerable to DOM changes or, if we are basing the selectors on CSS Styling, that the CSS Styling locators can change independently of the functionality and we may find functional based tests failing due to styling changes.getByRole
.const AudioLocator = {
source: "audio source"
}
expect(
element.querySelector(AudioLocator.source).
getAttribute("src")).
toEqual("https://eviltester.com")
AgGridTestUtils.js
code.const columnNamed = (cellName)=>{
return `.ag-cell[col-id="${cellName}"]`
}
waitFor
functionconst waitForGridToBeInTheDOM=()=>{
return waitFor(() => {
expect(document.querySelector(".ag-root-wrapper")).toBeInTheDocument();
});
}
waitForElementToBeRemoved
.PodcastGrid
component, I have to mock out fetch
requests when testing the component.test
function, then it may be time to rewrite it.test
function to cover the functionality first, then when you amend the code, you have a something that checks if you made any errors during any refactoring.PodcastGrid
to make it easier to understand how to automate AG Grid.it("renders user data from a url", async () => {
fetch
request made by the PodcastGrid
in a useEffect
to load the RSS feed.fakeRSSFeed
constant.const fakeRSSFeed =
`<channel>
<item>
<title>Fake Episode</title>
<pubDate>Thu, 23 Sep 2021 10:00:00 +0000</pubDate>
<enclosure url="https://eviltester.com"/>
<description>
<![CDATA[<p>Fake Description</p>]]>
</description>
</item>
</channel>`;
jest
is used to spy on any fetch
calls, and instead of issuing an HTTP request, simply return an object that represents the results of having made a fetch
.jest.spyOn(window, "fetch").mockImplementation(() =>{
return Promise.resolve({
text: () => fakeRSSFeed
})}
);
render(<PodcastGrid
rssfeed="https://fakefeed"
height="500px"
width="100%"
quickFilter=""/>);
await AgGridTest.waitForGridToBeInTheDOM();
await AgGridTest.waitForDataToHaveLoaded();
AgGridTestUtils.js
code.waitForGridToBeInTheDOM
waits for the basic grid to be present in the DOM.waitForDataToHaveLoaded
waits until any 'loading' indicator is no longer present.await AgGridTest.waitForPagination().
then((pagination)=>{
expect(pagination.firstRow).toEqual("1");
expect(pagination.lastRow).toEqual("1");
expect(pagination.rowCount).toEqual("1");
});
mp3
for the podcast episode has been rendered as an HTML audio control and has the URL from the RSS feed.waitFor
function because the audio control can be rendered asynchronously.// the audio component may take a little extra time to render so waitFor it
await waitFor(() => {
expect( AgGridTest.getFirstRowWithNamedCellValue("title", "Fake Episode").
querySelector(AgGridTest.columnNamed('mp3')).
querySelector(AudioLocator.source).
getAttribute("src")).
toEqual("https://eviltester.com")
}
)
fetch
function.// remove the mock to ensure tests are completely isolated
global.fetch.mockRestore();
});
querySelector
is a very flexible way to work with 3rd party componentsApp
component, which renders:PodcastGrid
component which renders AG GridPodcastGrid
would be tested in isolation. But I still wanted to make sure that when I click a feed from the dropdown, the grid is populated with the RSS feed.App
component will be tested in the DOM in isolation, but this approach demonstrates that it is possible to gradually build up 'integration' coverage without needing to do all the testing on a deployed application.userEvent
will hover over the element, priori to issuing the mouse events.it("loads feed into grid when button pressed", async () => {
fetch
again. In this case I have two RSS feed variables, because I want to make sure that when the drop down is selected, the url is populated in the text field and a call is made for the correct feed, rather than just returning the same RSS Feed regardless of the URL chosen.jest.spyOn(window, "fetch").mockImplementation((aUrl) =>{
if(aUrl==="https://feed.pod.co/the-evil-tester-show"){
return Promise.resolve({text: () => fakeEvilFeed});
}else{
return Promise.resolve({text: () => fakeWebrushFeed});
}
});
render(<App />);
select
drop down and url input
field.getByLabelText
and then the userEvent
abstraction layer to select a specific option from the drop down with selectOptions
.userEvent.selectOptions(screen.getByLabelText("Choose a podcast:"),'The Evil Tester Show');
Load Feed
button is clicked the props on the PodcastGrid
will be updated, causing the grid to re-render and load data into the grid.const loadButton = screen.getByText("Load Feed");
userEvent.click(loadButton);
await AgGridTest.waitForGridToBeInTheDOM();
await AgGridTest.waitForDataToHaveLoaded();
PodcastGrid
component has its own set of tests, all I do in the Integration test is the minimal assertion to check that the title of the correct podcast episode is present in the grid.// wait for first cell to expected data
await waitFor(() => {
expect(AgGridTest.getNamedCellsWithValues("title", "Fake Evil Tester Episode").length).toEqual(1);
});
// remove the mock to ensure tests are completely isolated
global.fetch.mockRestore();
});