This workshop is intended to be delivered in a workshop setting by an instructor either in person or via video recording. However, I'll try to document the steps/outline as best I can in this instructions document so you can try to work through it on your own if you like.
Follow the instructions in the README.md
file to set up the project.
There are 3 places where you'll find code that runs in production:
- client - runs in the browser. Entry at:
client/src/index.js
- server - runs on the server. Entry at:
server/index.js
- shared - runs in both. Entry at:
shared/index.js
To get things running, you'll use npm scripts
You can run npm run
to get a list of the available scripts. There are several
scripts in there that wont be entirely relevant to you during the workshop.
The main ones you should care about are:
npm run dev
- runs the dev server so you can work on and use the app in developmentnpm run test
- runs the unit and integration tests with jest in watch mode.npm run test:e2e
- runs the e2e tests with cypress in dev mode.
Course Topics
- Fundamentals behind tests and testing frameworks
- Distinctions of different forms of testing
- How to write Unit tests
- How to write Integration tests
- When and how to mock dependencies
- How to use test driven development to write new features
- How to use test driven development to find and fix bugs
- Core principles of testing to ensure your tests give you the confidence you need
See below in the shared content
See below in the shared content
See section called "Jest" below in the shared content. Keep it brief.
Instruction:
- Open
server/src/utils/__tests__/auth.todo.js
andserver/src/utils/auth.js
- Implement tests for
isPasswordAllowed
Exercise:
- Stay in
server/src/utils/__tests__/auth.todo.js
andserver/src/utils/auth.js
- Implement a single test for
userToJSON
Takeaways:
- Interact with the unit in the same way you would in the actual code. Then assert on the resulting value or changes in state.
- Pure functions are the easiest to unit test
- Test for use cases rather than for code coverage
- Using variables to be explicit about relationships is useful (when kept simple).
Mocking can be a little tricky, so we're going to approach it the same way we
approached learning what a testing framework is. In your terminal, change
directories to other/whats-a-mock
and run ./jest
. This will start jest in
watch mode for the files here. Review the thumb-war.js
and utils.js
files
then open __tests__/thumb-war.0.js
and follow the instructions there. Continue
through each of them. You'll find the solutions in the associated .solution
files.
New Things:
jest.mock
allows you to mock a dependencyjest.fn
allows you to create a function which keeps track of how it's calledjest.spyOn
allows you to wrap an object's function with a mock function.
Learn more about this from: "But really, what is a JavaScript mock?"
Takeaways:
- Mocks are simply fake versions of code that allow us to get coverage on code that may otherwise be very difficult or impossible to test reliably.
- Mocking dependencies reduces confidence that our application works
- Jest has an amazing mocking library
Extra Credit (old exercise):
Instruction:
- Open
server/src/utils/gist.js
andserver/src/utils/__tests__/gist.todo.js
- Implement an axios mock (inline with
jest.mock
) - Write the test and make assertions on the mock
- Remove the inline mock and show the existing
__mocks__/axios.js
file
Exercise:
- Open
server/src/utils/myjson.js
andserver/src/utils/__tests__/myjson.todo.js
- (Optionally) Implement an axios mock (inline with
jest.mock
) - Write the test and make assertions on the mock
- Remove the inline mock use the existing
__mocks__/axios.js
file
New Things:
beforeEach
allows you to run code before every test. There's alsoafterEach
, but using that can be less optimal in some situations. It's generally better to usebeforeEach
to clean up and prepare the environment for your test so if it fails the environment remains as it is at the time of the failure which can help debugging why the failure occurred.
Instruction:
- Open
server/src/controllers/__tests__/users.todo.js
andserver/src/controllers/users.todo.js
- Implement a test for
getUsers
andgetUser
- Demonstrate the test object factory pattern by extracting the common
req
, andres
setup to asetup
function
Exercise:
This one's optional based on how much time is available...
- Open
server/src/controllers/__tests__/posts.todo.js
andserver/src/controllers/posts.todo.js
- Implement a test for
getPosts
andgetPost
using test object factories
Takeaways:
- Multiple tests that look basically the same can be hard to maintain/understand
- Using a test object factory allows you to abstract some common code and leave only the code that's relevant for the test itself.
Instruction:
- Open
server/src/controllers/__tests__/users.todo.js
andserver/src/controllers/users.todo.js
- Implement a
deleteUser
async function using TDD.
Exercise:
- Open
server/src/controllers/__tests__/posts.todo.js
andserver/src/controllers/posts.todo.js
- Implement a
deletePost
async function using TDD.
Takeaways:
- Implement one part at a time to keep focused.
- Red, Green, Refactor (Don't forget the refactor!)
- Tests often have the basic shape of: Arrange, Act, Assert
New Things:
- Because integration tests are higher level, they require a bit more setup.
The
startServer
function accepts an options object. One option is theport
that should be used to start the server. It's important to specify that because when running the tests in parallel, it's impossible to know exactly which port other tests are using.
Instruction:
- Open
server/src/routes/__tests__/users.todo.js
andserver/src/controllers/users.js
- Implement requests to verify each of the users endpoints.
Exercise:
- Open
server/src/routes/__tests__/posts.todo.js
andserver/src/controllers/posts.js
- Implement requests to verify each of the posts endpoints.
Takeaways:
- Multiple assertions within a single test is often more pragmatic/practical than splitting things up into multiple tests.
- Integration often takes a fair amount more effort/setup, and has more points of failure, but the payoff is much greater.
New Things:
Instruction:
- Open
server/src/routes/users.js
and replace../controllers/users
with../controllers/users.bug.todo
(without anyone noticing?) - Run
npm run dev
and open the app. Note that the users endpoint is returning all of the user information (including thesalt
andhash
). - Open
server/src/routes/__tests__/users.todo.js
and add a test that reproduces the bug (note: this is the same test the attendees need to implement in their exercise). - Open the
server/src/routes/users.js
file again and note that the users endpoint codepath goes throughserver/src/controllers/users.bug.todo.js
. - Notice the bug in the users method.
Exercise:
- Open
server/src/routes/__tests__/users.bug.todo.js
andserver/src/controllers/users.bug.todo.js
- Implement the test for the bug fix first, then fix the bug
Takaways:
- Notice that we can be more certain that our code changes fixed the bug because we reproduced the failure in our tests and our code changes fixed the tests.
- Notice also that after we've manually verified things work as well, we should hopefully never have to do so again because the test is in place to ensure it wont break without failing the test.
- By implementing this as a higher level test, it was easier to write a test to find the bug without knowing exactly where the bug was or what was causing it.
See below in the shared content
- Fundamentals of what a test is and what role testing frameworks play
- Configure Jest for a client-side React project
- What Code Coverage is and how to properly use that metric
- Write unit tests for JavaScript utilities and React components
- What snapshot testing is and how to use it effectively
- Write integration tests for a React application
- Configure Cypress for a web application
- Write E2E (end-to-end) tests with Cypress
See below in the shared content
See below in the shared content
NOTE: This is duplicate content from the practices and principles workshop In this one however, folks should just watch the instructor go through things to make time for the rest of the content and not bore those who have already gone through this material.
See below in the shared content
Instruction:
- Nothing much here, direct people to the exercise and inform them they can use the solution for reference
Exercise:
- Start the simple react tests in watch mode with
npm run test:react
- Open
other/simple-react/item-list.js
andother/simple-react/__tests__/item-list.todo.js
- Follow the instructions to test the component
Takeaways
- The key here is to render the component and assert on the output.
- Assuming this were the only component for your entire application, attempt to use it the way the user would and let that inform your decisions of how you test it.
New Things:
- Code Coverage: A mechanism for us to understand how much of our code is run during the unit tests. 100% for libs, 70%ish for applications.
Instruction:
- Navigate to
./other/configuration/calculator
- Go ahead and run
npm run dev
and open uplocalhost:8080
to see the app npm install --save-dev jest
- Create a
test
script inpackage.json
tojest
- Run
npm test
-- No files found matching the defaulttestMatch
- Copy over
src/__tests__/utils.js
fromcalculator.solution
- Run
npm test
-- Fails due to syntax error with ES Modules which we have disabled for webpack - Update
.babelrc.js
to havemodules: 'commonjs'
in test mode. - Run
npm test
-- It works! - Add
console.log(window)
- Run
npm test
-- notice the huge window object is printed - Create a
jest
object property inpackage.json
and addtestEnvironment: 'node'
. - Run
npm test
-- notice it fails withwindow is not defined
which is what we want for node. - Remove
console.log(window)
Now let's deal with CSS imports:
- Copy
src/__tests__/auto-scaling-text.js
fromcalculator.solution
- Run
npm test
-- Fails because of the import of css - Create
jest.config.js
and move config frompackage.json
to that file. - Add
moduleNameMapper
to match.css
. Map it torequire.resolve('./test/style-mock')
- Create
style-mock.js
intest
directory. It needs no contents. - Run
npm test
-- The old error is gone! CSS importing is working, but now we're gettingdocument is not defined
. - Update
jest.config.js
totestEnvironment: 'jsdom'
. - Run
npm test
-- Passes!
Let's improve the CSS imports a bit:
- Add
console.log(div.outerHTML)
and notice there is no className because our style mock just returns an empty object for our css modules (I'm actually not sure why the style prop doesn't appear there... I guess React's not using the style attribute to apply those style properties?) npm install --save-dev identity-obj-proxy
- Add
moduleNameMapper
tojest.config.js
that matches.module.css
and maps toidentity-obj-proxy
(must come BEFORE the other one). - Run
npm test
-- Shows theclass
! - Remove the
console.log
because it's annoying.
Let's handle dynamic imports:
- Copy
src/__tests__/calculator.js
fromcalculator.solution
- Run
npm test
-- Fails due to syntax error on dynamic import npm install --save-dev babel-plugin-dynamic-import-node
- Update
.babelrc.js
to usedynamic-import-node
when in tests - Run
npm test
-- Fails becausewindow.localStorage
is not supported by JSDOM! - Copy
test/setup-test-framework.js
fromcalculator.solution
- Update
jest.config.js
to have asetupTestFrameworkScriptFile
that points torequire.resolve('./test/setup-test-framework')
- Run
npm test
-- Passes!
Ok! Now time for coverage!
- Update the
test
script inpackage.json
to bejest --coverage
- Run
npm test
-- Passes and includes coverage! - Open
./coverage/lcov-report/index.html
in a browser. Neat right!? It includes non-source files though - Update
jest.config.js
with acollectCoverageFrom
that is:['**/src/**/*.js']
- Run
npm test
-- Passes and includes coverage for only the files we care about.
Let's lock in our coverage!
- Update
jest.config.js
to have acoverageThreshold
of 70% for statements, branches, functions, and lines. - Run
npm test
-- Fails due to coverage threshold requirements - Update
jest.config.js
to have a more reasonablecoverageThreshold
- Run
npm test
-- Passes!
Let's turn on watch mode!
- Add a
test:watch
script topackage.json
and set it tojest --watch
- Run
npm run test:watch
- Explore Jest's amazing watch mode
Exercise:
No exercise here. It would be really boring I think...
Takeaways:
- Dependencies installed:
jest
,identity-obj-proxy
, andbabel-plugin-dynamic-import-node
- Get code coverage with:
jest --coverage
- Watch mode with:
jest --watch
- Configure jest with
jest.config.js
,--config
, orpackage.json
jest
property:"testEnvironment": "jest-environment-node"
if you don't needjsdom
collectCoverageFrom
to collect coverage numbers on your whole codebase (coveragePathIgnorePatterns
can ignore some)coverageThresholds
to keep your coverage from falling
Instruction:
- Open
client/src/screens/editor.todo.js
andclient/src/screens/__tests__/editor.todo.js
- Run the tests with
npm test editor.todo
- Implement the test (not the snapshot yet)
Exercise:
- Open
client/src/components/login.js
andclient/src/components/__tests__/login.step-1.todo.js
- Run the tests with
npm test login.step-1.todo
- Implement the login test
optional
- Open
client/src/components/__tests__/login.step-2.todo.js
- Run the tests with
npm test login.step-2.todo
- Use the utilities provided
Takeaways:
- TODO
Instruction:
- Start by explaining what snapshot tests even are (open
other/jest-expect/__tests__/expect-assertions.js
and go through the snapshots examples) - Open
client/src/screens/editor.todo.js
andclient/src/screens/__tests__/editor.todo.js
- Run the tests with
npm test editor.todo
- Implement the snapshot test
Exercise:
- Open
client/src/components/login.js
andclient/src/components/__tests__/login.step-1.todo.js
- Run the tests with
npm test login.step-3.todo
- Implement the snapshot test
Takeaways:
- TODO
Instruction:
- Explore the app code a little bit. Start at
client/src/app.js
- Open
client/src/__tests__/app.register.todo.js
- Run the tests with
npm test app.register.todo
- Implement the integration test
Exercise:
- Open
client/src/__tests__/app.login.todo.js
- Run the tests with
npm test app.login.todo
- Implement the integration test
Takeaways:
- TODO
Instructions:
- Change directories to
other/configuration/calculator
(further directories relative to this) - Run
npm install --save-dev cypress
- Run
npx cypress open
. Play around with it, then stop the process. - Explore
./cypress
Now let's have it run on our codebase
- In one terminal tab/window start the dev server
npm run dev
. Note this is running on port8080
- Open
./cypress.json
and add"baseUrl": "http://localhost:8080"
and"integrationFolder": "cypress/e2e"
npm install --save-dev cypress-testing-library
- Update
cypress/support/index.js
to importcypress-testing-library/add-commands
- Delete
./cypress/integration
and copy../calculator.solution/e2e/calculator.js
to./cypress/e2e/calculator.js
- Start cypress over again:
npx cypress open
and run the test. It passes!
Now let's make this a script
npm install --save-dev npm-run-all
- Add a
test:e2e:dev
script:npm-run-all --parallel --race dev cy:open
- Add a
cy:open
script:cypress open
- Run
npm run test:e2e:dev
. It works!
Now let's make this work for CI
- Add a
test:e2e
script:npm-run-all --parallel --race start cy:run
- Add a
cy:run
script:cypress run
- Add a
pretest:e2e
script:npm run build
- Run
npm run test:e2e
. It works!
Exercise:
No exercise here. It would be really boring I think...
Takeaways:
- TODO
New Things:
- The new script is
npm run test:e2e
- Cypress uses a mocha-like framework for tests (
describe
, andit
) - Cypress uses a chai-like assertion library.
- Cypress has an internal queueing system for it's commands. Each command can
yield a subject which allows you to execute commands on that subject. Think
of the
cy
global asuser
and you're giving the user instructions of what to do. You pretty much chain everything from one command to the other unless you want to context switch to a new task. learn more
Instruction:
- Open
cypress/e2e/auth.register.todo.js
and runnpm run test:e2e
- Run the tests
auth.register.todo.js
- Implement the register test
Exercise:
- Open
cypress/e2e/auth.login.todo.js
and runnpm run test:e2e
- Run the tests
auth.login.todo.js
- Implement the login test
Takeaways:
- Once you've verified registration works in the UI, you should avoid needless test bottlenecks by using a utility to register a new user rather than registering a new user with the UI.
- E2E tests allow you to use your app like a user which gives you a LOT more confidence that things will work as expected when a user does use your app.
- Cypress has an AMAZING UX for writing E2E tests for web apps!
See below in the shared content
Before we get into all the testing frameworks, let's learn about what a test
even is. In your terminal, change directories to other/whats-a-test
and open
the 0.js
file in your editor. Follow the instructions there and continue
through to 5.js
. You'll find the solutions in the associated .solution
files.
Learn more about this from: "But really, what is a JavaScript test?"
New Things:
- Assertion: A way for you to specify how things should be. Will throw an error if they are not that way, this is what fails the test.
Takeaways:
- Tests are simply code that runs other code and performs "assertions"
- Testing frameworks abstract this away for us to be more productive in writing tests.
Watch this 5 minute lightning talk: "What we can learn about testing from the wheel"
Instruction:
- Open
other/jest-expect/__tests__/expect-assertions.js
- Run
npm run test:expect
- Walk through the different assertions (should be pretty quick)
Exercise:
I don't think there's time/need for exercises here
Takeaways:
- Reference all the assertions here: https://facebook.github.io/jest/docs/en/expect.html
Take a look at other/coverage-example
. Look at the example.js
file and
compare it to the example.coverage.js
file. The one with coverage has been
instrumented with coverage meaning there's a variable set up for the file
and the code has been changed to include tracking of everywhere the code path
could go. Open up coverage/lcov-report/index.html
in a browser to see the
report that this is intended to create.
New Things:
- Branch: A branch in the code path. For example:
if
,else
,ternary
,switch
. - Statement: A syntax expression intended to be executed: Function call and/or assignment
- Lines: Basically irrelevant now
- Functions: Whether or not a function was ever invoked
Takeaways:
- Coverage is a useful metric as it shows you where code has not verifiably been run during tests.
- This metric is just an indicator and should not be misinterpreted as whether the logic is correct or the code will never break.
- You can get distracted by trying to achieve 100% code coverage when your time could be better spent elsewhere. Often trying to achieve 100% code coverage can result in doing weird things that make your tests brittle.
Basically this talk.