Testing
- Testing Pyramid: https://bit.ly/3dX0fap
- Many unit tests (which are fast and cheap)
- Fewer integration tests
- Even fewer end-to-end tests
- Unit tests thoroughly test logic and the basic operation of all your components.
- Tend to be “cheap”: Can be written and executed quickly.
- Dependencies often mocked (so as to test only the unit in question)
- Integration tests verify that the components integrate correctly.
- Passing tests with mocks is not helpful if the mocks are set up incorrectly.
- Suppose you write a unit test that passes a parameter “toyz” instead of “Toys” and also
configure the mock to look for the parameter “toyz”. Your unit test will pass but still have a bug.
- This is one reason why you write tests before you write code (so you don’t copy the bugs in your code over into the tests).
- This is also one reason why you watch your tests fail.
- End-to-end tests are the ultimate extension of integration tests: Watch everything all work together.
- Tend to be “expensive”:
- Complex to set up (e.g., database needs to be in a certain state)
- Often slow to run
- Need many fewer.
- In theory, you don’t need to provide every possible input to the end-to-end tests. Differences in logic / algorithms handled by unit tests. You just need to test the different workflows / integrations.
- Tend to be “expensive”:
Testing React
- High-level approach to unit testing:
- Render a component in a test environment, and
- Verify that the output is correct.
- The React Testing Library provides useful helpers
- Calling
render
returns an object with a lot of helpful objects / methods.let stuff = render(<AuthorForm/>)
- It is easier to use object deconstruction to just grab the items we need
let {container} = render(<AuthorForm/>)
container
is now the DOM element for the AuthorForm.- We can run typical HTML queries on it (e.g.,
getElementById
,querySelector
) render
also returns a list of query helpers that lets you do things like search for elements by text.- See https://testing-library.com/docs/react-testing-library/cheatsheet for details / documentation
-
The
fireEvent
method allows us to create interactions and test the resulting behavior. - Also note:
- Mocking the API
- use of
data-
attribute - CSS to access
data-
attribute - test feels more like an end-to-end test
- The
react-testing-library
intentionally doesn’t directly support “shallow” testing: Rendering a component and mocking its children.- They argue that this promotes tests that aren’t helpful.
- For example, the test focus more on what methods are called than whether calling those methods has the desired effect. (Think about how
AuthorList
andAuthorForm
interact – calling callbacks is meaningless unless it makes the desired action happen)
- Notice that there is no public interface to functions defined within components.
- In the case of Authors, there is relatively little logic — mostly just a sequence of method calls. In this case, see above comment about the usefulness of tests.
- In the case that there is a lot of logic, then that code should often be factored out into a helper class where it can be thoroughly tested.
- Keep in mind that there is certainly not agreement about this. You will have to figure out what makes sense given the specifics of the project and the culture of the company you work for.
End to End testing
- Basic idea of end-to-end tests for a web app is to use the app as the user would: By interacting with a web browser.
- Need three key pieces
- A web driver: A tool to programmatically interact with a browser.
- A test framework to specify what should be done (and the expected results)
- Some “glue” to translate the test steps into web driver actions.
- Selenium (https://www.selenium.dev/) is one of the most popular web drivers.
- There are several others.
- There are also libraries that abstract the different web drivers
- Capybara: https://teamcapybara.github.io/capybara/
- Some browsers need a 3rd party tool to interact with web drivers
- https://chromedriver.chromium.org/downloads
- On macOS you may have to override the quarantine:
xattr -d com.apple.quarantine chromedriver
- https://stackoverflow.com/questions/60362018/macos-catalinav-10-15-3-error-chromedriver-cannot-be-opened-because-the-de
- Cucumber https://cucumber.io is a platform for specifying end-to-end tests
- Used in BDD (Behavior Driven Development)
- Gherkin is the English-like language used to describe the desired behavior
- When I visit the root page
- Then I should see a list of authors
- Cucumber requires step definitions to translate Gherkin statements into web driver commands
Cucumber and JavaScript
- Download and install any necessary browser components: https://www.npmjs.com/package/selenium-webdriver
* On macOS you may have to override the quarantine:
xattr -d com.apple.quarantine chromedriver
- https://stackoverflow.com/questions/60362018/macos-catalinav-10-15-3-error-chromedriver-cannot-be-opened-because-the-de
- Install Selenium:
npm install --save-dev selenium-webdriver
- Install Cucumber:
npm install --save-dev cucumber
- Create a
features
directory - Create a
features/support
directory. -
Create a file (e.g.,
authors.feature
) and add a ScenarioFeature: Authors Scenario: Visit the root page When I visit the home page Then I should see the loading message When I wait for the authors to load Then I should see a list of authors
- Create a file
features/support/steps.js
- Launch your React and API servers
- This could also be automated; but, I’m trying to keep the example simple.
- Run
npx cucumber-js
- Note:
require('expect')
provides access to the Jest matchers- Most of the Selenium methods return promises.
- Notice the use of
then
after calls tofindElement
- Notice the use of
- The steps are expected to return a promise so that the runner knows how to progress
- The
/^..$/
pattern in the regular expression assures that the entire string is matched.- You could also just pass a string.
- Notice the
AfterAll
block to terminate the driver.- Sometimes it is helpful to leave it running for debugging.
- Comment out the
setCurrentAuthor(emptyAuthor)
line inAuthors.jsx
(currently line 50)- This was an actual bug I found when writing these sample tests
- Waiting.
- Waiting is a big challenge with external end-to-end tests.
- The test system needs to wait for the React updates to take place; but, it can be difficult to know when they are complete.
- Conventional approach is to wait for a change in the DOM to indicate the update took place.
- Waits need a timeout.
- If the timeout is too short, then you get false negatives
- If timeout is too long test run for a long time — especially if there are failures.
- Many approaches to getting a consistent / predictable set of test data.
- Test back-end API separately then mock out the back end.
- Not a true end-to-end test.
- Although there are advantages (mostly speed) to doing most of your e2e tests this way and having only a minimal number of “true” e2e tests to make sure the front end and and back end play nice.
- Have a dedicated test database that can be re-set between tests.
- Allows for more thorough e2e tests, but, can be very slow if the data set is complex (e.g, EPIC at Spectrum)
- Test back-end API separately then mock out the back end.
-
Show my e2e test setup for
blogAPI
- There are libraries that make it a bit easier:
- https://www.npmjs.com/package/jest-cucumber
- (Note: I haven’t used this yet.)
Cypress
- Runs right in the browser, so it makes the waiting issue less pronounced
- Calls are also asynchronous under the hood
- You can combine cypress with cucumber: https://wanago.io/2020/01/13/javascript-testing-cypress-cucumber/