In this tutorial we will test drive a React app which will use AWS Amplify to set up authentication and the backend API.
Test driving an application often starts at the bottom of the testing pyramid in unit tests. Unit tests focus on testing small units of code in isolation. However, this tutorial will start at the top of the pyramid with user interface (UI) testing. This approach is often called Acceptance Test Driven Development (ATDD).
There are a few benefits of starting at the top of the testing pyramid:
- Quick Feedback: Demonstrate a working system to the customer faster
- Customer Focus: Low level code clearly ties to high level customer value
- System Focus: The architecture evolves and expands on green.
-
Download and install Visual Studio Code
-
Open VS Code and set up the ability to launch VS Code from the terminal
-
Install Node Version Manager.
nvm
allows you to quickly install and use different versions of node via the command line. -
Run
nvm install node
to install the latest version of node -
Run
nvm use node
to use the latest version of node -
If you haven't already, create a GitHub account
-
Use the pairing4good/tdd-next-template template.
-
Click the
Use this template
button on the top right of pairing4good/tdd-next-template -
Click on
Settings > Code security and analysis
on your new repository- Enable
Dependabot alerts
- Enable
Dependabot security updates
- Enable
-
Update badges at the top of the
README.md
to point to your new repositories GitHub Action results
![Security Checks](https://github.com/{username}/{repository}/actions/workflows/codeql-analysis.yml/badge.svg)
![React Tests](https://github.com/{username}/{repository}/actions/workflows/node.js.yml/badge.svg)
![Cypress Tests](https://github.com/{username}/{repository}/actions/workflows/cypress.yml/badge.svg)
-
Update the
name
of your application in thepackage.json
file in the root of your repository -
Clone your new repository
Create a new Project and add it to your repository. Select the Board
layout. As you add stories, each story will be moved across the board from ToDo
to In Progress
to Done
. We show this progress so that anyone inside or outside the team can quickly see the progress that we are making.
The README.md
file is the first thing anyone sees when they open this repository. It's important to update your readme to include the following:
- Title
- Description of your product
- Install and run instructions
As a team member
I want to capture a note
So that I can refer back to it later
Given that no notes are entered
When nothing is saved
Then no notes should be listed
Given that one note exists
When a note is saved
Then two notes should be listed
Given a note exists
When the application is opened
Then a note is listed
Add this story to your Kanban board.
- Click the
+ Add item
link at the bottom of theTodo
column - Enter the short description of
Capture Note
- Type
Enter
- Click on the story name
Capture Note
- Click the
Edit
link within thedescription
section - Add the
As a
,I want
,So that
user story above - After the user story add an acceptance criteria section with the heading
Acceptance Criteria:
- Below that title add the
Given
,When
,Then
criteria - Click the
Update comment
button - Click the
x
button on the top right - Drag this new story from the
Todo
column into theIn Progress
column
This provides big and visible progress for everyone inside and outside the team. Teams meet around this board daily to align, formulate a plan for the day, and make any impediments big and visible.
The user story and acceptance criteria above describe a desired customer outcome. The user acceptance test will link this narrative with a high level how. For this tutorial our first application will be a web application built with React. The testing framework use to test this will be Cypress
- Rename
cypress/e2e/app.cy.js
tocypress/e2e/note.cy.js
- Open the
cypress/e2e/note.cy.js
file - Replace the contents of this file with the following
beforeEach(() => {
cy.visit('/');
});
describe('Note Capture', () => {
it('should create a note when name and description provided', () => {
cy.get('[data-testid=note-name-field]').type('test note');
cy.get('[data-testid=note-description-field]').type('test note description');
cy.get('[data-testid=note-form-submit]').click();
cy.get('[data-testid=test-name-0]').should('have.text', 'test note');
cy.get('[data-testid=test-description-0]').should('have.text', 'test note description');
});
});
-
Run
npm install
-
Run
npm run cypress:test
-
These commands are looking for elements on a webpage that contains a
data-testid
attribute with the value that follows the=
. We now have a failing acceptance test.
Timed out retrying after 4000ms: Expected to find element: [data-testid=note-name-field], but never found it.
- Our objective now is to make this test go green (pass) in as few steps as possible. The goal is not to build a perfectly designed application but rather to make this go green and then refactor the architecture through small incremental steps.
The first step to making this failing test go green is adding an element with one of the data-testid
's to the src/app/page.tsx
file.
export default function App() {
return (
<>
<input data-testid="note-name-field" />
</>
);
}
- Now the Cypress test fails on the second field
Timed out retrying after 4000ms: Expected to find element: [data-testid=note-description-field], but never found it.
- Add the next
input
field and rerun the test - Now the Cypress test fails on the submit button
Timed out retrying after 4000ms: Expected to find element: [data-testid=note-form-submit], but never found it.
- Add the
button
element with the expecteddata-testid
<input data-testid="note-name-field"/>
<input data-testid="note-description-field"/>
<button data-testid="note-form-submit" type="button">
Submit
</button>
- Now the Cypress test fails on the missing list of created notes
Timed out retrying after 4000ms: Expected to find element: [data-testid=test-name-0], but never found it.
In test driven development we do the simplest thing possible to make a test go green. Once it is green then and only then do we go back and refactor it.
In this case, the simplest thing that we can do is hard-code the expected values on the screen.
<input data-testid="note-name-field"/>
<input data-testid="note-description-field"/>
<button data-testid="note-form-submit" type="button">
Submit
</button>
<p data-testid="test-name-0">test note</p>
- Now the Cypress test fails on the note description
Timed out retrying after 4000ms: Expected to find element: [data-testid=test-description-0], but never found it.
- Add the final element for
test-description-0
export default function App() {
return (
<>
<input data-testid="note-name-field" />
<input data-testid="note-description-field" />
<button data-testid="note-form-submit" type="button">
Submit
</button>
<p data-testid="test-name-0">test note</p>
<p data-testid="test-description-0">test note description</p>
</>
);
}
- While this is far from a useful application, this application can be:
- refactored on green
- used to get feedback from the customer
Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. - Martin Fowler
The key to refactoring is to not change its "external behavior". In other words, after every change we make the test must remain green.
One "internal structure" change that could help, is pulling this form out into a react component so that we can drive these changes independently. Eventually src/app/page.tsx
will have several components:
<>
<Header />
<NoteForm />
<NoteList />
<Footer />
</>
So let's pull out a NoteForm
component.
- Create a new file called
noteForm.tsx
in thesrc/app
directory
export default function NoteForm() {
return <>//your form goes here</>;
}
-
This is a React functional component
-
The
export default
is the way to export only one object in ES6 -
Copy the form from
src/app/page.tsx
and paste it into the<></>
inNoteForm.js
<>
<input data-testid="note-name-field" />
<input data-testid="note-description-field" />
<button data-testid="note-form-submit" type="button">
Submit
</button>
<p data-testid="test-name-0">test note</p>
<p data-testid="test-description-0">test note description</p>
</>
- Replace the form contents in
src/app/page.tsx
with<NoteForm />
and add an import for theNoteForm
import NoteForm from "./noteForm";
export default function App() {
return <NoteForm/>
}
- Rerun you Cypress test and it is green
Congratulations, you've successfully made an internal structural change "without changing its external behavior" (Refactoring).