Pro Workshop

Web Application Testing

Web Application Testing

We all know the drill. The code works on your machine but you didn’t realize the impact of the change you made and it breaks in production. This problem grows exponentially as your application’s size and usefulness grows. Unpredictability can be a developer's worst nightmare.

However, many developers find it difficult to effectively test their code. Manual testing gets tedious fast. Complex logic and async code can be tricky to test effectively.

The Full Stack Testing Workshop will equip you with the tools you need to ensure your app works now and in the future. You'll learn how to write modular, maintainable tests that cover all of your code, from the backend to the frontend.

As the author of the de-facto standard testing library for web development (called eh… Testing Library), I have some solid opinions that have led many teams to much more confidence shipping their code. Let’s get you some of that confidence.

End-to-End Testing with Playwright

With end-to-end (E2E) testing, you can validate a web app from the user's perspective. This is the closest you can get to a real user interacting with your app. In this section, you'll learn the ins and outs of writing a test with Playwright:

  • Best practices for setup before running tests
  • Simulating user actions like interacting with the UI to perform a search
  • Creating a new user in the database
  • Cleaning up after tests have run
  • Using playwright fixtures for reliable setup/teardown

Mocking & Authentication for E2E Tests

When your application relies on external services, you need to have a way to test them without actually using them. Similarly, you don't want to fully onboard a new account every time you test an authenticated user flow. In this section, you'll learn how to re-use the same mocks we’ve been using for development to mock third-party services and simulate logging in:

  • Use Mock Service Worker to intercept network requests
  • Replace email verification with a local file solution
  • Attach cookies to Playwright
  • Reusing 2FA logic for local testing
  • Testing behavior with different HTTP responses

Unit Testing

Sometimes you’ve got some really complicated business logic in your app. Business logic that is critical to your application. For this stuff, it’s nice to package it up in its own function or module to keep all that logic in one place. This is the perfect situation where lower level unit testing comes in handy. Here, we'll dive into unit testing with the Vitest framework to ensure that each function and module behaves as expected:

  • Organizational best practices
  • Asserting outcomes with expect
  • Recording function calls with spies
  • Specifying test behavior with Vitest hooks
  • Keeping test output clean to avoid cluttering up the terminal

Component and Hook Testing

When building React components, you should focus on two users: the user who interacts with a component, and the developer who renders it. By using Testing Library, we can avoid mocking React and its hooks and instead test components as they would be used in a real app:

  • Render components with Testing Library
  • Simulate the DOM environment on a per-test basis
  • Querying for missing elements
  • Testing hooks in isolation from React
  • Stubbing React context
  • Cleanup after testing

Authenticated Integration

Similar to higher-level E2E tests, it is often necessary to simulate an authenticated user. However, we don't want to test the entire user flow every time we make a change. In this section, you'll learn how to test authenticated operations without the overhead of a full E2E test:

  • Generating random session IDs
  • Associating sessions with users
  • Testing authenticated routes
  • Asserting user state in responses

Custom Assertions

Writing tests is good; writing readable and maintainable tests is better. Custom assertions can make your test code easier to understand and modify. Here you'll learn to encapsulate complex checks into simple, understandable assertions:

  • Create matchers for specific states
  • Share assertions across tests
  • Add custom assertions to TypeScript definitions
  • Format test output with colors for readability

Test Database

There are some decisions to make when writing tests that interact with the database. Testing with the development database has benefits, but requires us to remember to delete data after each test. By using a test database, we can avoid accidents while still being able to run our test suites:

  • Set up a test-specific database instance
  • Seeding a minimal amount of data
  • Defining a global setup for Vitest
  • Understanding the importance of module import order