31
loading...
This website collects cookies to deliver better user experience
render
method and the screen
object to test the basics of your component. For interactions with the component, I'm also using userEvent
from @testing-library/user-event.EntitiesComponent
.import { render, screen } from '@testing-library/angular';
it('renders the entities', async () => {
await render(EntitiesComponent);
expect(screen.getByRole('heading', { name: /Entities Title/i })).toBeDefined();
// Use the custom Jest matchers from @testing-library/jest-dom
// to make your tests declarative and readable
// e.g. replace `toBeDefined` with `toBeInTheDocument`
expect(screen.getByRole('cell', { name: /Entity 1/i })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: /Entity 2/i })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: /Entity 3/i })).toBeInTheDocument();
});
screen
object. You can think of screen
as the real screen an end-user would see (the DOM tree), containing multiple queries to verify that the component is rendered correctly. The most important query is the byRole
variant, it lets you select the element just as how a user (or screen reader) would. Because of this, it has the added benefit to make your components more accessible.💡 TIP: use screen.debug()
to log the HTML in the console, or use screen.logTestingPlaygroundURL()
to create an interactive playground. For example, the example application used in this article is available with this playground link. The playground helps to use the correct query.
TableComponent
) to render the entities.import { render, screen } from '@testing-library/angular';
it('renders the entities', async () => {
await render(EntitiesComponent, {
declarations: [TableComponent],
providers: [
{
provide: EntitiesService,
value: {
fetchAll: jest.fn().mockReturnValue([...])
}
}
]
});
expect(
screen.getByRole('heading', { name: /Entities Title/i })
).toBeInTheDocument();
expect(
screen.getByRole('cell', { name: /Entity 1/i })
).toBeInTheDocument();
expect(
screen.getByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
expect(
screen.getByRole('cell', { name: /Entity 3/i })
).toBeInTheDocument();
})
TestBed
, the added configuration of render
(the second argument) must feel familiar. That's because render
is a simple wrapper around the TestBed
and the API is kept identical, with some smart defaults.EntitiesService
service is stubbed to prevent that the test makes an actual network request. While we write component tests, we don't want external dependencies to affect the test. Instead, we want to have control over the data. The stub returns the collection of entities that are provided during the test setup. Another possibility would be to use Mock Service Worker (MSW). MSW intercepts network requests and replaces this with a mock implementation. An additional benefit of MSW is that the created mocks can be re-used while serving the application during the development, or during end-to-end tests.import {
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
it('renders the entities', async () => {
await render(EntitiesComponent, {
declarations: [TableComponent],
providers: [
{
provide: EntitiesService,
value: {
fetchAll: jest.fn().mockReturnValue([...])
}
}
]
});
expect(
screen.getByRole('heading', { name: /Entities Title/i })
).toBeInTheDocument();
expect(
screen.getByRole('cell', { name: /Entity 1/i })
).toBeInTheDocument();
expect(
screen.getByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
expect(
screen.getByRole('cell', { name: /Entity 3/i })
).toBeInTheDocument();
userEvent.type(
screen.getByRole('textbox', { name: /Search entities/i }),
'Entity 2'
);
// depending on the implementation:
// use waitForElementToBeRemoved to wait until an element is removed
// otherwise, use the queryBy query
await waitForElementToBeRemoved(
() => screen.queryByRole('cell', { name: /Entity 1/i })
);
expect(
screen.queryByRole('cell', { name: /Entity 1/i })
).not.toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
})
userEvent
object.type
method, the following events are fired: focus
, keyDown
, keyPress
, input
, and keyUp
.userEvent
, you can use fireEvent
from @testing-library/angular
.waitForElementToBeRemoved
.waitForElementToBeRemoved
must only be used when an element is asynchronously removed from the document.queryBy
query and assert that the element does not exist in the document. The difference between the queryBy
and getBy
queries is that getBy
will throw an error if the DOM element does not exist, while queryBy
will return undefined
if the element does not exist.findBy
queries can be used.queryBy
queries, but they're asynchronous.💡 TIP: To make test cases resilient to small details, I prefer to use the findBy
queries over the getBy
queries.
fakeAsync
and tick
utility methods from @angular/core/testing
.it('renders the table', async () => {
jest.useFakeTimers();
await render(EntitiesComponent, {
declarations: [TableComponent],
providers: [
{
provide: EntitiesService,
useValue: {
fetchAll: jest.fn().mockReturnValue(
of([...]),
),
},
},
],
});
expect(
await screen.findByRole('heading', { name: /Entities Title/i })
).toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 1/i })
).toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 3/i })
).toBeInTheDocument();
userEvent.type(
await screen.findByRole('textbox', { name: /Search entities/i }),
'Entity 2'
);
jest.advanceTimersByTime(DEBOUNCE_TIME);
await waitForElementToBeRemoved(
() => screen.queryByRole('cell', { name: /Entity 1/i })
);
expect(
await screen.findByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
});
import {
render,
screen,
waitForElementToBeRemoved,
within,
waitFor,
} from '@testing-library/angular';
import { provideMock } from '@testing-library/angular/jest-utils';
import userEvent from '@testing-library/user-event';
it('renders the table', async () => {
jest.useFakeTimers();
await render(EntitiesComponent, {
declarations: [TableComponent],
providers: [
{
provide: EntitiesService,
useValue: {
fetchAll: jest.fn().mockReturnValue(of(entities)),
},
},
provideMock(ModalService),
],
});
const modalMock = TestBed.inject(ModalService);
expect(
await screen.findByRole('heading', { name: /Entities Title/i })
).toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 1/i })
).toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
expect(
await screen.findByRole('cell', { name: /Entity 3/i })
).toBeInTheDocument();
userEvent.type(
await screen.findByRole('textbox', { name: /Search entities/i }),
'Entity 2'
);
jest.advanceTimersByTime(DEBOUNCE_TIME);
await waitForElementToBeRemoved(
() => screen.queryByRole('cell', { name: /Entity 1/i })
);
expect(
await screen.findByRole('cell', { name: /Entity 2/i })
).toBeInTheDocument();
userEvent.click(
await screen.findByRole('button', { name: /New Entity/i })
);
expect(modalMock.open).toHaveBeenCalledWith('new entity');
const row = await screen.findByRole('row', {
name: /Entity 2/i,
});
userEvent.click(
await within(row).findByRole('button', {
name: /edit/i,
}),
);
waitFor(() =>
expect(modalMock.open).toHaveBeenCalledWith('edit entity', 'Entity 2')
);
});
userEvent.click
method to simulate a user click on the button.provideMock
is used from @testing-library/angular/jest-utils
to mock a ModalService
. provideMock
wraps every method of the provided service with a jest mock implementation.within
and waitFor
.within
method is used because there's an edit button for every row in the table.within
we can specify which edit button we want to click, in the test above it's the edit button that corresponds with "Entity 2".waitFor
, is used to wait until the assertion inside its callback is successful.waitFor
we can wait till that happens.render
method.screen
object and the utility methods to assert that the directive does what it's supposed to do.appSpoiler
directive which hides the text content until the element is being hovered.test('it is possible to test directives', async () => {
await render('<div appSpoiler data-testid="sut"></div>', {
declarations: [SpoilerDirective],
});
const directive = screen.getByTestId('sut');
expect(screen.queryByText('I am visible now...')).not.toBeInTheDocument();
expect(screen.queryByText('SPOILER')).toBeInTheDocument();
fireEvent.mouseOver(directive);
expect(screen.queryByText('SPOILER')).not.toBeInTheDocument();
expect(screen.queryByText('I am visible now...')).toBeInTheDocument();
fireEvent.mouseLeave(directive);
expect(screen.queryByText('SPOILER')).toBeInTheDocument();
expect(screen.queryByText('I am visible now...')).not.toBeInTheDocument();
});
MockStore
we have the best of both worlds.MockStore
in a component test.provideMockStore
method is used, in which we can overwrite the results of the selectors that are used within the component.import { render, screen } from '@testing-library/angular';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
it('renders the table', async () => {
await render(EntitiesComponent, {
declarations: [TableComponent],
providers: [
provideMockStore({
selectors: [
{
selector: fromEntities.selectEntities,
value: [...],
},
],
}),
],
});
// create a mock for `dispatch`
// this mock is used to verify that actions are dispatched
const store = TestBed.inject(MockStore);
store.dispatch = jest.fn();
expect(store.dispatch).toHaveBeenCalledWith(fromEntities.newEntityClick());
// provide new result data for the selector
fromEntities.selectEntities.setResult([...]);
store.refreshState();
});
💡 TIP: If you're following Single Component Angular Modules, it easier to see when changes have an impact on your tests.