Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Dockerfile.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Step 1: Build Jekyll Site
FROM ruby:3.3.4-bullseye AS builder

WORKDIR /app

COPY Gemfile /app/
COPY Gemfile.lock /app/
RUN bundle install

CMD [ "bundle", "exec", "jekyll", "serve", "-w", "--host", "0.0.0.0", "-P", "4000" ]

143 changes: 111 additions & 32 deletions _posts/2025-01-31-tester-pattern-react.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
---
layout: post
title: "[WIP] The tester pattern in react"
description: "Explorint the testern pattern in a react project"
category: "develop"
tags: ["javascript", "typescript", "react", "vite", "react-testing-library"]
title: "[WIP] Applying the Tester Pattern in React Testing"
description: "Exploring the testern pattern in a React project"
category: develop
tags: ["JavaScript", "TypeScript", "react", "vite", "react-testing-library"]
---

The [tester pattern](https://www.testerpattern.nl/pattern) is a good way to structure your tests to improve the readability and to make very easy to write and expand the test suite.
The [tester pattern](https://www.testerpattern.nl/pattern) is a great way to
structure your tests to improve readability and making it easier to write
and expand the test suite.

In the original webpage, the author explain the concepts in java, in this blog we will try the pattern in JavaScript, more specifically in a react SPA.
In the original webpage, the author explains the concepts in Java, in this blog
we will apply the pattern in JavaScript, specifically in a React SPA.

The libraries that we will be using are:

- vitest as the runner and mock library
- react testing library to interact with react components.
- React testing library to interact with React components.

Let's explore the pattern in various examples with an increase level of
complexity:
Let's explore the pattern with examples of increasing complexity:


- a simple component that draws a static message
- a form with various inputs.
- the same component, but the message is a quote from a web service. Using MSW for the test.
- the same component, but the message is a quote from a web service. Using MSW
for the test.


# first scenario: simple component
Expand Down Expand Up @@ -54,11 +57,12 @@ describe('simpleComponent', () => {
});
```

For this simple case the boilerplate of adding a tester and asserter helper
classes may be too much, but an interesting side-effect is that the test doesn't
have any component-related selector.
For this simple case, adding tester and asserter helper classes may seem like
overkill. However, an interesting side effect is that the test does not rely on
any component-specific selectors

The test is in 'plain' english (with the limitation of the language) and the helper classes can be user for multiple tests.
The test is in 'plain' English (with the limitation of the language) and the
helper classes can be user for multiple tests.

The tester and asserted used:

Expand Down Expand Up @@ -113,8 +117,7 @@ new BookTester()

To map this to a `react-testing-library` test, we need to use async/await,
normally the asserter methods are required to be async in order to use the
[findBy
methods](https://testing-library.com/docs/dom-testing-library/api-async#findby-queries),
[findBy methods](https://testing-library.com/docs/dom-testing-library/api-async#findby-queries),
so we need to migrate to:

```typescript
Expand All @@ -130,9 +133,8 @@ await asserter.thenHasInCover("Author: Arturo Volpe");
await asserter.thenHasInChapters(2);
```

Maybe if someday the [pipeline
operator](https://github.com/tc39/proposal-pipeline-operator?tab=readme-ov-file)
added to the language, this will be more concise:
If the [pipeline operator](https://github.com/tc39/proposal-pipeline-operator?tab=readme-ov-file)
added to the language, this syntax could become more concise:

```typescript
new BookComponentTester()
Expand All @@ -145,11 +147,16 @@ new BookComponentTester()
|> await %.thenHasInChapters(2)
```






# Second scenario: A simple form

The tester pattern realy shines when we need to test different use cases that
has a similar setup and need specific assertions, in this case we will have a
form to edit some personal information (name and lastname).
The Tester Pattern really shines when testing multiple use cases that share a
similar setup but require specific assertions. In this case, we will test a form
for editing personal information (first name and last name).

For the form we will use
[react-hook-form](https://react-hook-form.com/get-started), and the [getting
Expand All @@ -159,7 +166,7 @@ modifications.
In this form, we have two inputs:

* name: the first name, required, max length 10
* lastname: the last name, not required, max length 20
* last name: the last name, not required, max length 20

```tsx
type Inputs = {
Expand Down Expand Up @@ -223,8 +230,8 @@ instructions, and the tester execute the required steps.

Reading this test we don't know anything about the internals of the component,
we only know that we are asking the tester to submit the form with a given name
and lastname. **The internals of the component and the complexity of the
emulation of user actions is hidden to the test, making it easy to read and
and last name. **The internals of the component and the complexity of the
emulation of user actions is hidden to the test, making it easy to read and
follow**.

The tester for this form is slightly more complex:
Expand Down Expand Up @@ -271,11 +278,83 @@ class Step2FormPageAsserter {
}
```

We can see that there are some 'component kwnoledge' in the Tester, the tester
knows how to find the desired inputs, and that logic, specially when we are
testing front end components is very tricky, sometimes we need to wait it to
render, sometimes we need to use a complex css selector, but all of that is
hidden and is a implementation detail of the Tester.
The Tester encapsulates some 'component knowledge'—it knows how to find the
required inputs. This logic can be complex, especially when testing frontend
components. Sometimes, we need to wait for rendering; other times, we must use a
complex CSS selector. However, all of this is hidden as an implementation detail
within the Tester.

## A note about the Tester for React Components

We can use the [Page Object Model](https://playwright.dev/docs/pom) popularized
by tools like selenium or playwright to create the tester. This is more similar
to what a user would do when using the component.

> In the original tester pattern, we are testing a action, like calling an
> endpoint or a method, but now we are testing the user actions, so it's better
> to test a sequence of actions, in this example the filling of a form.


Using this pattern we can have another approach, instead of giving the Tester all
the information that it need to perform the action, we instruct it like a robot:

```tsx
describe('step2Form', () => {
it('renderValidValuesRobot', async () => {

const tester = new Step2FormPageRobotTester();

await tester.givenComponentRendered();
await tester.givenFirstname('Arturo');
await tester.givenLastname('Volpe');

let asserter = await tester.whenDoSubmit();
await asserter.hasAllFieldsValid();
});

it('firstName.inmediateFeedback', async () => {

const tester = new Step2FormPageRobotTester();

await tester.givenComponentRendered();
let asserter = await tester.whenFirstnameFilled('');

await asserter.hasMessage('Firstname is required');
});
});

class Step2FormPageRobotTester {

component!: RenderResult;

givenComponentRendered() {
this.component = render(<Step2Form />);
}
givenFirstname(targetName: string) {
return this.fillInput("firstname-input", targetName);
}
givenLastname(targetName: string) {
return this.fillInput("lastname-input", targetName);
}

async whenDoSubmit() {
const bttn = await this.component.findByText('Save');
fireEvent.click(bttn);
return new Step2FormPageAsserter(this.component);
}

async fillInput(targetTestId: string, toInsert: string) {
const input = await this.component.findByTestId(targetTestId);
fireEvent.change(input, {target: {value: toInsert}})
return this;
}

async whenFirstnameFilled(targetName: string) {
await this.fillInput("firstname-input", targetName);
return new Step2FormPageAsserter(this.component);
}
}
```

## Adding more tests cases

Expand Down Expand Up @@ -347,7 +426,7 @@ class Step2FormPageTester {
}
```

> Here we can have two differente asserts, one to check for contents in the
> Here we can have two different asserts, one to check for contents in the
page, and another one to assert the invocations to the function. For simplicity,
we will only use one.

Expand Down Expand Up @@ -405,4 +484,4 @@ Links:
* Component: [Step2Form.tsx](https://github.com/aVolpe/vitest-tester-pattern-playground/blob/main/src/Step2Form.tsx)
* Test: [Step2Form.test.tsx](https://github.com/aVolpe/vitest-tester-pattern-playground/blob/main/src/Step2Form.test.tsx)

All the source code for the examples is in the [vitest pattern playground github repo](https://github.com/aVolpe/vitest-tester-pattern-playground/tree/main)
All the source code for the examples is in the [vitest pattern playground github repo](https://github.com/aVolpe/vitest-tester-pattern-playground/tree/main)
11 changes: 10 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '2'
services:
blog:
build: .
Expand All @@ -11,3 +10,13 @@ services:
- LETSENCRYPT_HOST=www.volpe.com.py,blog.volpe.com.py
- LETSENCRYPT_EMAIL=arturovolpe@gmail.com

blog-devel:
build:
context: .
dockerfile: Dockerfile.local
ports:
- "4000:4000"
expose:
- 4000
volumes:
- ./:/app/
2 changes: 2 additions & 0 deletions run-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
docker compose up blog-devel