Skip to main content
Skip table of contents

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.

What problem did web components solve that React components could not?

A large portion of the http://VA.gov site is built through the content-build application, which uses Liquid templates. The teams building the Liquid templates had created their own implementations of the React components. This meant that

  • these implementations did not get any upgrades or fixes that were applied to the React components

  • implementations of these components were uneven across the static pages

  • teams were responsible for fixing a variety of issues on their own, including accessibility issues, which were also unevenly applied

By moving to web components, we

  • reduced the amount of work required to maintain component implementations

  • improved consistency across component implementations, including consistency between static pages and React applications

The Design System Team has done our best to make adoption as easy as possible.

  • We’ve added React bindings so that React teams are able to use the components without too much extra work.

  • We’ve added migration scripts that automate the migration from the React component to the web component.

  • As we release new web components, we’ve been reaching out to teams who use the older React version and encouraging them to migrate. In some cases we have assisted with that process as well.

As teams have adjusted to the use of the web components, we’ve seen the adoption rate increase as well. For example, the va-loading-indicator component has been adopted pretty quickly and most of the migrations were done by the frontend teams.

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-fetch and sinon.

          CODE
          import 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.

  • Use callbacks to signal test step completion or to handle events. Like Mocha’s done():

    CODE
    it('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 window properties. 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:

          CODE
          it('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 a before() or beforeEach() hook, where it can be used multiple times:

          CODE
          describe('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, with src/platform/testing/e2e/mock-helpers.js being one such example.

  • vets-api endpoints use rspec for 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 new url redirect

    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() blocks

    • Extensive 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/await with waitFor may result in the conditionals of waitFor to not be properly processed before test scripts proceed.


JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.