Long post alert!
Hi all! Yesterday during the community call there was a brief discussion on testing, and from what I understood; the config and setup for jest has been ping-ponged around a bit, but there were some troubles setting up. I’d love to help out setting up. It would be great if someone (I believe @peterp and @thedavid worked on this?) could clarify what the difficulties were and how we could jump in!
Other than getting stuff to run I have some more thoughts on making testing easier/more convenient for Redwood users!
Jest
Web
Low Hanging Fruit
-
Stub SVG/Image imports.
-
Expose a custom
render
function that includesRedwoodProvider
andRouter
stuff.
Mocking
I believe it was @rob who mentioned that in rails it’s common to not mock anything except the database. I like this thought! I like to mock as little as possible to keep tests as close to reality as possible.
At the moment I noticed scaffolds generate tests in the following shape:
import { render, cleanup, screen } from '@testing-library/react'
import { Loading, Empty, Failure, Success } from './PostsCell'
describe('PostsCell', () => {
afterEach(() => {
cleanup()
})
it('Loading renders successfully', () => {
render(<Loading />)
// Use screen.debug() to see output.
expect(screen.queryByText('Loading...')).toBeInTheDocument()
})
it('Empty renders successfully', () => {
render(<Empty />)
expect(screen.queryByText('Empty')).toBeInTheDocument()
})
it('Failure renders successfully', () => {
render(<Failure error={{ message: 'Oh no!' }} />)
expect(screen.queryByText('Error: Oh no!')).toBeInTheDocument()
})
it('Success renders successfully', () => {
render(
<Success posts={[{ title: 'Post Title' }]} />
)
expect(
screen.queryByText('Post 1 ')
).toBeInTheDocument()
})
})
This tests all the units of the Cell, but it doesn’t hit the QUERY part! You could turn it into something like this:
// ========== Mocking the API ==========
import { render } from '@redwood/testing'
import Posts from './PostsCell'
describe('PostsCell', () => {
it('Loading renders successfully', () => {
const { getByText } = render(<Posts />)
// It should immediately start to load
expect(getByText('Loading...')).toBeInTheDocument()
})
it('Empty renders successfully', async () => {
const mocks = [
{
request: {
query: QUERY,
variables: {},
},
result: {
data: { posts: [] },
},
},
]
const { findByText } = render(<Posts />, { mocks })
// After resolving without posts, "Empty" will be displayed
expect(await findByText('Empty')).toBeInTheDocument()
})
it('Failure renders successfully', async () => {
const mocks = [
{
request: {
query: QUERY,
variables: {},
},
error: new Error('Whoops'),
},
]
const { findByText } = render(<Posts />, { mocks })
// After resolving with an error, the error state will be displayed
expect(await findByText('Error: Oh no!')).toBeInTheDocument()
})
it('Success renders successfully', async () => {
const mocks = [
{
request: {
query: QUERY,
variables: {},
},
result: {
data: {
posts: [
{
title: 'Post 1',
},
],
},
},
},
]
const { findByText } = render(<Posts />, { mocks })
// After retrieving the posts, display them!
expect(await findByText('Post 1')).toBeInTheDocument()
})
})
This is a mocking method for ApolloClient
using MockProvider
(abstracted away): https://www.apollographql.com/docs/react/development-testing/testing/
It mocks any QUERY
’s with the exact variables passed and returns the defined data.
This is pretty neat, but what if the mocked result doesn’t match the API by accident? Or my API changes. I’d need to update all the mocks of the domain that changed. MirageJS has a neat way of mocking API’s that works in all test frameworks. It’s kind of like what @mojombo mentioned in yesterday’s call about using factories to create stuff in your (fake) database.
Mirage JS is an API mocking library that lets you build, test and share a complete working JavaScript application without having to rely on any backend services.
I love using this tool in a corporate setting where frontend and backend development is completely split. By using mirage we can mimick the backend without relying on another team.
Buuuuut… in this setting we don’t rely on another team. We have our own backend! Instead of rewriting our entire backend in something like Mirage, why can’t we just use our actual backend? You could do something like this:
// ========== No mocking but the database. ==========
import { render, makeServer } from '@redwood/testing'
import Posts from './PostsCell'
describe('PostsCell', () => {
let server
beforeEach(() => {
server = makeServer()
})
it('Loading renders successfully', () => {
const { getByText } = render(<Posts />)
// It should immediately start to load
expect(getByText('Loading...')).toBeInTheDocument()
})
it('Empty renders successfully', async () => {
const { findByText } = render(<Posts />)
expect(await findByText('Empty')).toBeInTheDocument()
})
it('Failure renders successfully', () => {
const mocks = [
{
request: {
query: QUERY,
variables: {},
},
error: new Error('Whoops'),
},
]
const { getByText } = render(<Posts />, { mocks })
expect(getByText('Error: Oh no!')).toBeInTheDocument()
})
it('Success renders successfully', async () => {
server.create('post')
const { getByText, findByText } = render(<Posts />)
// The loading state test could be combined into this one!
expect(getByText('Loading...')).toBeInTheDocument()
// After a while, posts will be loaded.
expect(await findByText('Post 1')).toBeInTheDocument()
})
})
That would be cool! I’m not sure if it’s possible, maybe this is even too much, but I think it maybe, possibly could be done by intercepting requests using Pretender (This is also what Mirage uses), somehow forwarding those requests to an Apollo Test Client, no HTTP server required, should be able to run in Jest since it’s NodeJS, then add stuff to the database through Redwood services (or a “factory” layer on top of services?) or maybe even through the Prisma Client (or a “factory” layer on top of the Prisma Client?).
A big tradeoff is of course speed. But I’d rather have good tests than fast tests (there is a limit to my patience of course : - )). One thing that could make this easier is In-Memory SQLite databases in Prisma
Another thing that makes both example 2 and 3 difficult at the moment is the rendering of Cells. @peterp @mojombo do you think it’s possible to move the processing of Cells from a Webpack Loader to Babel? This would make it usable in Jest as well!
Api
I’m not very experienced with backend testing, but I guess in a similar fashion it would be great to
- Insert into DB.
- Check response of queries are correct.
And
- Call mutations.
- Check database contains what you expect it to contain.
Cypress
In Cypress you could do very similar things, I’ve already created a little experiment for that! The difference is that there’s no weird setup required; you just run your full app front to back to database.
One thing to consider here (and in Jest tests as well) would be mocking Auth. If you’re using Auth0 there’s some tutorials out there telling you to create a separate M2M tenant to programmatically obtain tokens for using in your e2e tests. I don’t really like that approach because you’re still hitting the 3rd party endpoint from your automated test regularly, which could generate troubles with rate limits and all that. Another thing I like to do is create a new fake user for every test (as opposed to having a file with precreated users), which would fill up your tenant DB pretty quickly, tasking you with wiping it somehow every so often. I’m not very experienced with this however, so please correct me if I’m wrong!
Another thing I was struggling with is how to run these tests in your CI/CD pipeline. With the new Netlify beta it’s now possible to run Cypress tests post-build (awesome). How can I tell my already deployed app to use a different database (wouldn’t want to run automated tests on my production database I guess). Perhaps intercepting requests with Pretender and forwarding them to a node process running in the post-build job? But wouldn’t that be mocking?
Conclusion
Most importantly I’d like to help creating an initial setup so people can decently run some tests!
Anyway look at the time, I g2g! If you read my entire post; thanks! Please let me know what you think. (Also if you didn’t read the entire post!)
Cheers!