Just Our Type

Cypress Network Stubbing with GraphQL and TypeScript

Zach Panzarino
Dandy Engineering, Product & Data Blog
17 min readAug 7, 2023

--

Photo by CDC on Unsplash

Background

At Dandy, we use the Cypress framework for our end-to-end front-end testing. As anyone who’s worked with Cypress testing in a real codebase before knows, it can take some serious infrastructure to make test writing as easy as Cypress’s docs promise it will be. One of the most important systems to get right is network stubbing, which at Dandy means playing nicely with TypeScript and GraphQL. This post will walk you through our stubbing setup and highlight how easy it is to create your own GraphQL Code Generator plugin.

To Stub or Not To Stub

Network stubbing in “end-to-end” tests? As an end-to-end test framework, Cypress gives you the ability to run through an application just as an end-user would. While this style of testing more closely resembles your production behavior, these tests can become cumbersome to maintain as you set up test databases and environments. Furthermore, running these tests can significantly add to continuous integration build times.

We make significant use of stubbed End-to-End Tests. Cypress allows you to control every aspect of a network response. This means that while we can stub a network delay in Cypress, we also are not making calls over the wire and responses can be returned quickly. This flexibility allows us to write fewer expensive true end-to-end tests while being able to write many faster tests to expand coverage.

require('stuff');

At the most fundamental level there are three considerations that we wanted our GraphQL stubbing system needs to fulfill:

  • All custom commands and mock responses need to be fully typed
  • It should be easy to set or modify operation responses at any time in a test
  • It should be easy to assert that our application sent a GraphQL request with the correct variables

That’s it! Seems straight-forward, right? Well, providing this developer experience actually requires a bit of magic under the hood to make everything come together nicely. So, if you value an enjoyable (yes, enjoyable) test writing experience in a type-safe environment, then read on!

GraphQL Operation Typing

The hardest part of designing our GraphQL stubbing system was figuring out how to maintain type safety throughout. Before we get into the details of how we maintain types in stubs, let’s go over how we generate GraphQL types for our application (source) code.

Let’s assume we’ve defined the following example GraphQL operations that we want to use in our frontend clients:

query OrdersByIds($ids: [String!]!) {
ordersByIds(ids: $ids) {
...LabOrder
}
}

mutation PlaceLabOrder($data: PlaceLabOrderCommand!) {
placeOrder(data: $data) {
...PlacedOrder
}
}

As is fairly standard practice, we use graphql-codegen to automatically turn our .graphql files into both type definitions and React hooks. We’ll ignore the React hooks for now, since we want to stub requests at the network level, so we actually won’t modify any Apollo Client middleware in our tests.

We use the typescript-operations plugin to generate types for all of our graphql operations. We’ll omit the full config here since the options we choose aren’t relevant but suffice it to say that after the plugin runs we’ll get an output that looks something like this:

export type OrdersByIdsQueryVariables = Types.Exact<{
ids: Array<Types.Scalars['String']>;
}>;

export type OrdersByIdsQuery = { __typename?: 'Query', ordersByIds: Array<(
{ __typename?: 'LabOrder' }
& LabOrderFragment
)> };

export type PlaceLabOrderMutationVariables = Types.Exact<{
data: Types.PlaceLabOrderCommand;
}>;

export type PlaceLabOrderMutation = { __typename?: 'Mutation', placeOrder: Array<(
{ __typename?: 'LabOrder' }
& PlacedOrderFragment
)> };

So far, so good. The generated code is a bit of an eyesore, but it’s pretty clear that we’ve directly translated our GraphQL operations into Typescript types. We now have both the variables that we need to send when calling a given operation as well as return types for those operations.

This seems like all we would need to start creating custom commands for stubbing right? Well, not quite. One glaring omission from this generated code is a way of actually associating the name of an operation with its types. We as humans reading the code know that the OrdersByIdsquery will return data which fits the type OrdersByIdsQuery, but the code does not.

We’ll want to generate some sort of mapping of operation name to return/input type that looks like this:

interface MockQueryTypes {
...
OrdersByIds: OrdersByIdsQuery
...
}

interface MockQueryVariablesTypes {
...
OrdersByIds: OrdersByIdsQueryVariables
...
}

After exploring the graphql-codegen plugins registry for a while, there were no plugins that immediately jumped out as being designed for this purpose. So, we just decided to build our own!

The Custom Codegen Plugin

ℹ️ The official docs for writing your first plugin are a good place to start learning about how codegen plugins work.

Before we dive into the plugin itself, let’s continue defining the requirements a little more closely. We already know what output we want to generate, but we also want to provide some configuration options. In this case, we really need just a single piece of additional functionality: the ability to import generated types from another package, so we can avoid defining them twice. We define the following interface, which uses the same option as typescript-operations and typescript-react-apollo, making the plugins trivial to combine in the same codegen config if needed:

interface MockOperationPluginConfig {
importOperationTypesFrom?: string;
}

We want to keep the types that are actually used in application code separate from our testing types, so being able to import from a different package is essential for our plugin. We’ll revisit this later and explain how it all comes together.

Ok. Now that we have some requirements, it’s time to build this thing. Remember, we’re mapping an operation name to the corresponding variables and return type for that operation. If we look at the code we generated earlier, it becomes clear that the output type names are just the operation names with either Query or Mutation appended to the end (with Variables variants, too).

That seems pretty straightforward. We just need to associate an operation name with the type of that operation, and then we can turn that association into code. We’ll define the following interface to represent that association:

import { OperationTypeNode } from 'graphql';

// we're not supporting subscriptions, so we exclude it from the type
// this results in the type: 'query' | 'mutation'
type OperationType = Exclude<OperationTypeNode, 'subscription'>;

interface OperationInfo {
name: string;
operation: OperationType;
}

Simple enough. Now, let’s start writing the plugin itself. The function signature is pretty straightforward, and this is boilerplate for all plugins.

import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
import { GraphQLSchema } from 'graphql';

export const plugin: PluginFunction<MockOperationPluginConfig> = (
// we'll ignore the schema in our plugin
schema: GraphQLSchema,
// an array of documents for each of our operations
documents: Types.DocumentFile[],
// config type we defined earlier
config: MockOperationPluginConfig
) => {
...
}

Within that function, we’ll iterate through the documents provided to us to create an array of OperationInfo instances (the association of operation name to operation type).

import { isExecutableDefinitionNode, Kind } from 'graphql';
import _ from 'lodash';

const documentOperations: OperationInfo[] = documents.flatMap(d => {
if (!d.document) {
return [];
}
// we use _.compact to remove any undefined values
return _.compact(
d.document.definitions.map(node => {
// find each operation definition that's not a subscription
// and return the simplified association
if (
isExecutableDefinitionNode(node) &&
node.kind === Kind.OPERATION_DEFINITION &&
node.name &&
node.operation !== 'subscription'
) {
return {
name: node.name.value,
operation: node.operation,
};
}
})
);
});

The docs around these AST types are not super straightforward, so I recommend opening this code in a real editor. From there, Typescript is your best friend, and combing through the types makes it far easier to understand how this actually works. If the above code works properly, we should be left with an array that resembles something like the following:

const documentOperations: OperationInfo[] = [{
name: 'OrdersByIds',
operation: 'query'
}, {
name: 'PlaceLabOrder',
operation: 'mutation'
}];

Now that we have some clear cut associations, we should have enough information to actually start generating our output as code. In order to do this we’re just going to generate our Typescript code as one big string. We define the following function to create a single key/type pair which we’ll use to build a larger interface with a full mapping.

// this is the exact same function used by the `typescript-operations` plugin, so we use it here too
import { pascalCase } from 'change-case-all';

// isVariables corresponds to whether we're generating the types for the input variables (true) or the output (false)
const getDefinition = ({ name, operation }: OperationInfo, isVariables: boolean = false): string => {
// if we have somewhere to import the types from, use that location as a prefix, otherwise don't use a prefix
// ex: config.importOperationTypesFrom = 'Types' -> 'Types.'
const importTypesFrom = config.importOperationTypesFrom ? `${config.importOperationTypesFrom}.` : '';

// get the name of the types which we can expect to have been already generated for these operations
// if isVariables is true, append Variables as we're generating the types for the input variables
// ex: name = 'PlaceLabOrder', operation: 'mutation' -> 'PlaceLabOrderMutation'
const typeBaseName = `${pascalCase(`${name}_${operation}${isVariables ? 'Variables' : ''}`)}`;

// combine our type name with the place that we need to import from, assuming one is provided
// ex: 'Types.PlaceLabOrderMutation'
const typeName = `${importTypesFrom}${typeBaseName}`;

// finally, output as an key/value entry in an object
// ex. 'PlaceLabOrder: Types.PlaceLabOrderMutation'
return `${name}: ${typeName}`;
};

Perfect! Each result is clearly designed to be an entry in what will eventually be our final interface. There are only a couple more steps to pull everything together.. We’ll start by generating this definition line for all of our operations:

// split operations by type
const [queryOperations, mutationOperations] = _.partition(
documentOperations,
({ operation }) => operation === 'query'
);

// generate definitions for both the return types and the parameters
const mockQueryDefinitions = queryOperations.map(operation => getDefinition(operation));
const mockQueryVariablesDefinitions = queryOperations.map(operation => getDefinition(operation, true));
const mockMutationDefinitions = mutationOperations.map(operation => getDefinition(operation));
const mockMutationVariablesDefinitions = mutationOperations.map(operation => getDefinition(operation, true));

The final step is to take these definitions and output them as code within interfaces. We achieve this through some crude string templating. It might not be the prettiest, but it works and the generated code is all correct.

return `
export interface MockQueryTypes {
${mockQueryDefinitions.join('\\n ')}
}
export interface MockMutationTypes {
${mockMutationDefinitions.join('\\n ')}
}
export interface MockQueryVariablesTypes {
${mockQueryVariablesDefinitions.join('\\n ')}
}
export interface MockMutationVariablesTypes {
${mockMutationVariablesDefinitions.join('\\n ')}
}
`;

And that’s it! We’ve written a full plugin in just a couple of lines that should do everything we need to tie in type safety, response mutations, and variable checking. Let’s add this plugin to our config and generate some code so that we can actually move on to testing!

By the way, here’s the full script:

import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
import { pascalCase } from 'change-case-all';
import { isExecutableDefinitionNode, Kind, OperationTypeNode, GraphQLSchema } from 'graphql';
import _ from 'lodash';

type OperationType = Exclude<OperationTypeNode, 'subscription'>;

interface OperationInfo {
name: string;
operation: OperationType;
}

interface MockOperationPluginConfig {
importOperationTypesFrom?: string;
}

export const plugin: PluginFunction<MockOperationPluginConfig> = (
schema: GraphQLSchema,
documents: Types.DocumentFile[],
config: MockOperationPluginConfig
) => {
const documentOperations: OperationInfo[] = documents.flatMap(d => {
if (!d.document) {
return [];
}
return _.compact(
d.document.definitions.map(node => {
if (
isExecutableDefinitionNode(node) &&
node.kind === Kind.OPERATION_DEFINITION &&
node.name &&
node.operation !== 'subscription'
) {
return {
name: node.name.value,
operation: node.operation,
};
}
})
);
});

const getDefinition = ({ name, operation }: OperationInfo, isVariables: boolean = false): string => {
const importTypesFrom = config.importOperationTypesFrom ? `${config.importOperationTypesFrom}.` : '';
const typeBaseName = `${pascalCase(`${name}_${operation}${isVariables ? 'Variables' : ''}`)}`;
const typeName = `${importTypesFrom}${typeBaseName}`;
return `${name}${type === 'operation' ? '?' : ''}: ${typeName}`;
};

const [queryOperations, mutationOperations] = _.partition(
documentOperations,
({ operation }) => operation === 'query'
);

const mockQueryDefinitions = queryOperations.map(operation => getDefinition(operation));
const mockQueryVariablesDefinitions = queryOperations.map(operation => getDefinition(operation, true));
const mockMutationDefinitions = mutationOperations.map(operation => getDefinition(operation));
const mockMutationVariablesDefinitions = mutationOperations.map(operation => getDefinition(operation, true));

return `
export interface MockQueryTypes {
${mockQueryDefinitions.join('\\n ')}
}
export interface MockMutationTypes {
${mockMutationDefinitions.join('\\n ')}
}
export interface MockQueryVariablesTypes {
${mockQueryVariablesDefinitions.join('\\n ')}
}
export interface MockMutationVariablesTypes {
${mockMutationVariablesDefinitions.join('\\n ')}
}
`;
};

ℹ️ Note: You’ll have to actually build the code using tsc before you can proceed and use the plugin in the codegen process.

We define the following config using our plugin and the import-types-preset:

generates:
mock-operations-types.generated.ts:
schema: '...'
preset: import-types
presetConfig:
# package containing already generated operation types
typesPath: '@dandy/graphql-operations'
documents:
- '...'
plugins:
# path to plugin we just defined
- '.../mock-operations-plugin'
config:
importOperationTypesFrom: 'Types'

After running graphql-codegen with that config, we get the following output:

import * as Types from '@dandy/graphql-operations';

export interface LabsGqlMockQueryTypes {
OrdersById: Types.LabsGqlOrdersByIdsQuery
}
export interface LabsGqlMockMutationTypes {
PlaceLabOrder: Types.LabsGqlPlaceLabOrderMutation
}
export interface MockQueryVariablesTypes {
OrdersByIds: Types.OrdersByIdsQueryVariables
}
export interface LabsGqlMockMutationVariablesTypes {
PlaceLabOrder: Types.LabsGqlPlaceLabOrderMutationVariables
}

We now have the foundational types we need to build out our Cypress GraphQL stubbing utilities. While this took a fair amount of set up, it enables us to fully leverage the power of the type system to create easy to use utilities.

Cypress Utils

Now that we’ve got our foundation set up, it’s time to put together some utils that should make it easy to stub GraphQL within tests. Before we do so, let’s outline the use cases that are important to us in order to properly test our application:

  • We can set a response for an operation, and that response will be used for the remainder of the test unless modified.
  • We can validate that an operation was called with the correct input variables.

Request Structure and Mock Responses

Before we worry about responding to a request, let’s explore what a request looks like when it comes in.

We use Apollo on both the client and server side to send and receive information using GraphQL. Using our fairly standard configuration, intercepted requests will have a body resembling the following example:

{
operationName: 'OrdersById',
query: 'query OrdersByIds($ids: [String!]!) { ...', // includes the full graphql code
variables: {
ids: ['abc123', 'xyz789']
}
}

Simple enough, and we clearly have enough information in the body of the request alone to achieve all of the objectives that we set out above. Let’s take a look at how we can achieve fully typed mock responses and add responses to requests.

The first thing we’ll need to do is actually call cy.intercept to intercept the request. We’re not going to want to do this manually in every test, so we’ll create a wrapper function that will get turned into a custom command. Here’s the simple boilerplate that we’ll start with:

import { CyHttpMessages } from 'cypress/types/net-stubbing';

const setupGraphqlMocking = () => {
cy.intercept('POST', '/graphql*', (req: CyHttpMessages.IncomingHttpRequest) => {

});
};

We POST all of our requests regardless of operation type, and all operations hit the same /graphql endpoint (we use the wildcard here to account for potential query params).

Now, we need to determine how to respond to each request. The simplest way that we can do this is by setting up a dictionary to keep track of the response value for a given operation name. Thanks to the types that we generated earlier, it’s simple to do this in a strongly typed fashion. Then, we can take the operation name from the request that we intercept and retrieve the corresponding result from the dictionary. At that point, all we have to do is send that result in a reply. That works out to look something like the following:

import { CyHttpMessages } from 'cypress/types/net-stubbing';

const setupGraphqlMocking = () => {
// we use a Partial type since we generally won't have a response set for all operations
const mockedQueries: Partial<MockQueryTypes> = { };
const mockedMutations: Partial<MockMutationTypes> = { };

const getResponse = (operationName: string): Record<string, any> => {
// while we generally like to avoid casting, in this case it's perfectly safe and keeps things simple
if (operationName in mockedQueries) {
return mockedQueries[operationName as keyof MockQueryTypes] ?? {};
}
if (operationName in mockedMutations) {
return mockedMutations[operationName as keyof MockMutationTypes] ?? {};
}
return {};
};

cy.intercept('POST', '/graphql*', (req: CyHttpMessages.IncomingHttpRequest) => {
const response = getResponse(req.body.operationName);
req.reply({ data: response });
});
};

Pretty basic — request comes in, send back a response. There’s just one problem: we haven’t provided the user with a way to actually set the response for any operations! Without that we’ll just be replying with an empty object every time!

We’ll get to that in a second, but before we do there is one important thing to call out about the above function: we house the response dictionaries inside of the setupGraphqlMocking function. This is intentional because it ensures that you have a clean slate every time and no responses accidentally hang around from one test to another, which could happen when using something like a global variable. This concept of test isolation will continue to guide some of the design decisions that we make as we further build out this system.

Because setupGraphqlMocking is designed to be executed as a custom command, and we also want additional custom commands to set responses, we’ll need to design those response commands in a unique way. Fortunately, Cypress provides some utilities that actually make this really easy.

The first thing to do is write sub-functions within setupGraphqlMocking to modify the stored queries and mutations. These are simple setter functions with types thrown on top for good measure.Next, we’ll use Cypress aliases so that we can access them later on. While we could do something like assign these functions to global variables, that again opens the door for artifacts to hang around after a test has completed, whereas all aliases are automatically cleared between every test.

It might not be obvious from the docs, but you can actually alias any type of object or value with the as command; it doesn’t have to be a DOM element or anything special. With a little bit of wrap magic, this results in the following additions to our function:

const setupGraphqlMocking = () => {
// we use a Partial type since we generally won't have a response set for all operations
const mockedQueries: Partial<MockQueryTypes> = { };
const mockedMutations: Partial<MockMutationTypes> = { };

// intercepting here
...

const setGraphqlQueryMock = <Name extends keyof MockQueryTypes>(name: Name, mock: MockQueryTypes[Name]) => {
mockedQueries[name] = mock;
};
const setGraphqlMutationMock = <Name extends keyof MockMutationTypes>(name: Name, mock: MockMutationTypes[Name]) => {
mockedMutations[name] = mock;
};

// we hide these statements from the log since they're not particularly important or helpful
// and they'll show for every test, polluting the command log
cy.wrap(setGraphqlQueryMock, { log: false }).as('setGraphqlQueryMock');
cy.wrap(setGraphqlMutationMock, { log: false }).as('setGraphqlMutationMock');
};

Now that we have that part set up, we’ll need to define the functions that’ll become our custom commands. We’ll define these at the top-level, outside of the function that we’ve been exclusively working in so far. All they have to do is get the alias and call that function.

// we cast the results of getting the alias since Cypress' types mark it as JQuery<HTMLElement>
// while that is the most common use case for aliases, it's not how we're using them so the cast should be safe
// it's also not really best practice to use the type of something within its definition as we do here
// but it works and it's easy

const setGraphqlQueryMock = <Name extends keyof MockQueryTypes>(name: Name, mock: GqlMockQueryTypes[Name]) => {
return cy.get('@setGraphqlQueryMock').then(setMock => {
(setMock as unknown as typeof setGraphqlQueryMock)(name, mock);
});
};

const setGraphqlMutationMock = <Name extends keyof MockMutationTypes>(name: Name, mock: MockMutationTypes[Name]) => {
return cy.get('@setGraphqlMutationMock').then(setMock => {
(setMock as unknown as typeof setGraphqlMutationMock)(name, mock);
});
};

One added benefit of the alias approach is that these commands will actually fail if the alias isn’t set. This forces the consumer to make sure that they’ve properly set up mocking through the setup function before trying to do anything with those mocks. Now that we have all the functions that we need, the last step is to actually add these as Cypress commands. When we do this we make sure to not just add the command but also register the types as described in the docs.

declare global {
namespace Cypress {
interface Chainable {
setupGraphqlMocking: typeof setupGraphqlMocking;
setGraphqlQueryMock: typeof setGraphqlQueryMock;
setGraphqlMutationMock: typeof setGraphqlMutationMock;
}
}
}

Cypress.Commands.add('setupGraphqlMocking', setupGraphqlMocking);
Cypress.Commands.add('setGraphqlQueryMock', setGraphqlQueryMock);
Cypress.Commands.add('setGraphqlMutationMock', setGraphqlMutationMock);

Woohoo! We now finally have a fully typed system for stubbing GraphQL requests! We can now guarantee that any mock response we set must be for a legitimate operation and the response satisfies the expected type.

While being able to easily provide typed responses was the main goal here, there are a couple more utilities that we can build off of this foundation. In particular, we want to be able to validate that a certain request was made by our application, and that the application provided the correct variables when making that request. In order to do that, we first have to alias the incoming requests.

Aliasing Requests

When a request comes in we’ll immediately want to tag it so that we can easily work with it later. Cypress calls these “aliases,” and it’s remarkably easy to set one from within their network stubbing framework. In fact, they already have an example of how to do this with GraphQL, which we’ll use as a starting point for our implementation.

We want to alias each incoming request using the name of the operation and the type of the operation so that it’s a bit easier to work with later. We’ll also want to prefix each of our requests with gql for disambiguation with any other aliases and to keep the command log a bit cleaner. Given the above information, we’re able to come up with the following function for getting an alias name for any given request:

import _ from 'lodash';

const QUERY_REGEX = /^query/;
const MUTATION_REGEX = /^mutation/;

const isQueryRequest = (queryContent: string) => QUERY_REGEX.test(queryContent);
const isMutationRequest = (queryContent: string) => MUTATION_REGEX.test(queryContent);

// this is a separate function since we'll also use it later on to get requests by their alias
const getAlias = (operationName: string, operationType: 'Query' | 'Mutation' | null): string => {
return `gql${operationName}${operationType ?? ''}`;
};

const getReqAlias = (req: CyHttpMessages.IncomingHttpRequest): string => {
if (isQueryRequest(req.body.query)) {
return getAlias(req.body.operationName, 'Query');
}
if (isMutationRequest(req.body.query)) {
return getAlias(req.body.operationName, 'Mutation');
}
// fallback in case neither of the above conditions match, which should never happen
return getAlias(req.body.operationName, null);
};

Now we just have to augment our existing setupGraphqlMocking function to actually set the alias on the requests when they come in.

import { CyHttpMessages } from 'cypress/types/net-stubbing';

const setupGraphqlMocking = () => {
...
cy.intercept('POST', '/graphql*', (req: CyHttpMessages.IncomingHttpRequest) => {
req.alias = getReqAlias(req);

const response = getResponse(req.body.operationName);
req.reply({ data: response });
});
...
};

One small thing to note here: while the alias is actually set through this method, the Cypress command log often includes “no alias” next to the request. This is incorrect. The alias is being set, it just doesn’t show up for some reason, so don’t worry about it if you notice this as well.

Now that we have our aliases, we can build out commands to verify that a given operation was called, and was called with the correct input variables. In order to do this, we’ll use the wait command to wait for the request to complete, and then chain that with an assertion. This is where those Variables types that we generated earlier will come in handy, as they’ll allow us to type the params of our command.

export const graphqlQueryShouldBeCalledWith = <Name extends keyof MockQueryTypes>(
name: Name,
value: MockQueryVariablesTypes[Name]
) => {
return cy.wait(`@${getAlias(name, 'Query')}`)
.its('request.body.variables')
.should('deep.equal', value);
};

export const graphqlMutationShouldBeCalledWith = <Name extends keyof MockMutationTypes>(
name: Name,
value: MockMutationVariablesTypes[Name]
) => {
return cy.wait(`@${getAlias(name, 'Mutation')}`)
.its('request.body.variables')
.should('deep.equal', value);
};

We’ll also add these as custom commands, just as we did with our first set above:

declare global {
namespace Cypress {
interface Chainable {
graphqlQueryShouldBeCalledWith: typeof graphqlQueryShouldBeCalledWith;
graphqlMutationShouldBeCalledWith: typeof graphqlMutationShouldBeCalledWith;
}
}
}

Cypress.Commands.add('graphqlQueryShouldBeCalledWith', graphqlQueryShouldBeCalledWith);
Cypress.Commands.add('graphqlMutationShouldBeCalledWith', graphqlMutationShouldBeCalledWith);

And we’re done! We finally have a fully typed system for stubbing GraphQL requests in Cypress. We’ll leave the actual test writing itself as an exercise for the reader 😉.

While what we’ve developed here is a good starting point, there are many ways to build upon this foundation and build out a truly remarkable system. One improvement that immediately jumps out is adding properties to setupGraphqlMocking so that you can set responses at setup time without having to call a different command. In the same vein, it’s also extremely helpful to have a large set of default response fixtures for the most common operations. These are particularly useful for end-to-end tests where the app might be making requests not directly related to the active test but still needs to render without error.

All of these additional improvements form our set of GraphQL mocking utilities here at Dandy. We take particular pride in making it easy for our engineers to follow best practices and write high quality code, and investing in the test writing experience is just one of the ways that we make that happen.

--

--