Platform Best Practices - Unit and e2e Tests
Last Updated: May 26, 2025
The Platform’s test suites form the backbone of our continuous integration (CI) pipeline. By following the guidance in this page, VFS teams can ensure that the tests for their applications/forms are running both efficiently and effectively.
Consideration for these practices should include recognition of the Platform’s transition from React to web components, as lead by the Design System Team. Unit tests for application functionality layered atop web components may be warranted, but web components themselves are already comprehensively tested by Design System Team engineers.
Unit Testing
Unit tests should validate logic within individual components or functions, not UI behavior or rendering.
Unit tests are designed to validate internal logic, not external dependencies like API calls. Introducing API calls can lead to brittle tests that fail due to network issues or external system changes.
Mock APIs and external dependencies to ensure unit tests remain focused and isolated.
Reset mocks and stubs between tests to avoid state leakage.
This can be accomplished with the use of
node-fetchandsinon.CODEimport chai, { expect } from 'chai'; import sinon from 'sinon'; import fetch from 'node-fetch'; import { fetchUser } from '../src/api.js'; describe('fetchUser', () => { let stub; beforeEach(() => { stub = sinon.stub(global, 'fetch'); }); afterEach(() => stub.restore()); it('resolves JSON when ok', async () => { stub.resolves({ ok: true, json: async () => ({ id: 1 }) }); const user = await fetchUser(1); }); it('rejects on non-ok', () => { stub.resolves({ ok: false, status: 404 }); return expect(fetchUser(2)).to.be.rejectedWith('Network error: 404'); }); }); const callback = sinon.spy(); myAsyncFunction(arg, callback); // later on in the test… sinon.assert.calledOnce(callback); sinon.assert.calledWith(callback, expectedValue);
Testing dates and times
Dates and times can be mocked for better reliability that isn’t vulnerable to leap days, daylight savings, AWS region migrations, or other unexpected circumstances.
Utilize date utility libraries to manage time-sensitive logic effectively.
mockdateis one such tool already used invets-website.
Use callbacks to signal test step completion or to handle events. Like Mocha’s
done():CODEit('reads a file via callback', done => { fs.readFile('foo.txt', 'utf8', (err, data) => { expect(err).to.be.null; expect(data).to.equal('hello'); done(); }); });
Enzyme has been deprecated, as it is incompatible with React 19+. Platform now supports the use of the React Testing Library (RTL).
Legacy tests using Enzyme may eventually require phased updates to keep up with Platform standards.
The upcoming Node 22 upgrade may present issues for tests that manipulate
windowproperties. Specific mocks may be required to handle such test cases.
Cypress e2e Testing
Teams should use E2E tests for complex interactions or full application flows.
This is where you want to perform shadow DOM testing. Cypress support for the testing of shadow DOM elements is far more robust than Mocha. Some teams may encounter situations where some unit tests would be best converted into e2e tests to ensure effective test coverage.
Real API calls can be used in e2e tests, but this choice introduces a risk of test failures resulting from network issues or external system changes.
The
cy.intercept()Cypress command can be used to stub out your app’s network requests during testing. By returning predefined responses instantly, it both avoids flaky real‑server calls and speeds up your test suite.This command can be used anywhere before a request would be fired:
Inside an
it()block:CODEit('shows error after failed login', () => { cy.intercept('POST', '/api/login', { statusCode: 401 }).as('loginFail'); cy.get('button[type=submit]').click(); cy.wait('@loginFail'); });Inside a
describe()block as part of abefore()orbeforeEach()hook, where it can be used multiple times:CODEdescribe('My flow', () => { before(() => { cy.intercept('GET', '/api/profile', { data: [] }).as('getProfile'); }); it('does something that triggers that request', () => { cy.visit('/'); cy.wait('@getProfile'); // … }); });
Larger response objects to be mocked can be kept in a separate file and imported as necessary.
There are several helper files already in use throughout
vets-website, withsrc/platform/testing/e2e/mock-helpers.jsbeing one such example.vets-apiendpoints userspecfor test automation. While Cypress should not be used to establish adequate endpoint test coverage, there may be cases where teams find value in using real API calls in their e2e tests.
Potential pain points
If
cy.visit()is the last command used in a test block, Cypress may hang if a redirect occurs, like in this example:
Cypress hangs because it is not expecting the /education/submit-school-feedback/configuration page.
An assertion can be added after such a command to ensure that the redirect is occurring as expected.
Overly large
it()blocksExtensive testing performed in a single block may be convenient for certain reasons, but it can result in a flaky test spec.
Stack traces used during debugging may not provide useful direction beyond identifying the block containing problematic code.
If evaluating a flow that logs in, reaches user profile data, checks messages, then searches for a completed form, what was the failure point? Login? Session timeout? An unexpected redirect?
Using test users to perform real logins within test specs
This is another opportunity to use mocks. Test user data can be manipulated by anyone with access to the TUD, so specs relying on static test user credentials and user state will be vulnerable to unintentional interference.
Using
async/awaitwithwaitFormay result in the conditionals ofwaitForto not be properly processed before test scripts proceed.
Help and feedback
Get help from the Platform Support Team in Slack.
Submit a feature idea to the Platform.