// __tests__/login.js
import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import Login from '../login'
describe('Login', () => {
let utils,
handleSubmit,
user,
changeUsernameInput,
changePasswordInput,
clickSubmit
beforeEach(() => {
handleSubmit = jest.fn()
user = {username: 'michelle', password: 'smith'}
utils = render(<Login onSubmit={handleSubmit} />)
changeUsernameInput = value =>
userEvent.type(utils.getByLabelText(/username/i), value)
changePasswordInput = value =>
userEvent.type(utils.getByLabelText(/password/i), value)
clickSubmit = () => userEvent.click(utils.getByText(/submit/i))
})
describe('when username and password is provided', () => {
beforeEach(() => {
changeUsernameInput(user.username)
changePasswordInput(user.password)
})
describe('when the submit button is clicked', () => {
beforeEach(() => {
clickSubmit()
})
it('should call onSubmit with the username and password', () => {
expect(handleSubmit).toHaveBeenCalledTimes(1)
expect(handleSubmit).toHaveBeenCalledWith(user)
})
})
})
describe('when the password is not provided', () => {
beforeEach(() => {
changeUsernameInput(user.username)
})
describe('when the submit button is clicked', () => {
let errorMessage
beforeEach(() => {
clickSubmit()
errorMessage = utils.getByRole('alert')
})
it('should show an error message', () => {
expect(errorMessage).toHaveTextContent(/password is required/i)
})
})
})
describe('when the username is not provided', () => {
beforeEach(() => {
changePasswordInput(user.password)
})
describe('when the submit button is clicked', () => {
let errorMessage
beforeEach(() => {
clickSubmit()
errorMessage = utils.getByRole('alert')
})
it('should show an error message', () => {
expect(errorMessage).toHaveTextContent(/username is required/i)
})
})
})
})
The definition of test fixtures is separated from their assignment, and they can and often are reassigned at different levels of nestedness.
The setup logic is separated from the tear down; the code is grouped together based on when it's executed vs what aspect of behavior it's testing.
Because it's not always clear what tests require what setup, once the setup/teardown code is there, it tends to stay there even when it's no longer needed.
Not all test that can be logically grouped require the same kind of setup; balancing whether to "share" the setup code vs have it in a specific test creates a lot of mental friction.
test('calls onSubmit with the username and password', () => {
const {handleSubmit, user} = setupSuccessCase()
expect(handleSubmit).toHaveBeenCalledTimes(1)
expect(handleSubmit).toHaveBeenCalledWith(user)
})
test('shows an error message when submit is clicked and no username is provided', () => {
const {handleSubmit, errorMessage} = setupWithNoUsername()
expect(errorMessage).toHaveTextContent(/username is required/i)
expect(handleSubmit).not.toHaveBeenCalled()
})
test('shows an error message when password is not provided', () => {
const {handleSubmit, errorMessage} = setupWithNoPassword()
expect(errorMessage).toHaveTextContent(/password is required/i)
expect(handleSubmit).not.toHaveBeenCalled()
})
function setup() {
const handleSubmit = jest.fn()
const utils = render(<Login onSubmit={handleSubmit} />)
const user = {username: 'michelle', password: 'smith'}
const changeUsernameInput = value =>
userEvent.type(utils.getByLabelText(/username/i), value)
const changePasswordInput = value =>
userEvent.type(utils.getByLabelText(/password/i), value)
const clickSubmit = () => userEvent.click(utils.getByText(/submit/i))
return {
...utils,
handleSubmit,
user,
changeUsernameInput,
changePasswordInput,
clickSubmit,
}
}
function setupSuccessCase() {
const utils = setup()
utils.changeUsernameInput(utils.user.username)
utils.changePasswordInput(utils.user.password)
utils.clickSubmit()
return utils
}
function setupWithNoPassword() {
const utils = setup()
utils.changeUsernameInput(utils.user.username)
utils.clickSubmit()
const errorMessage = utils.getByRole('alert')
return {...utils, errorMessage}
}
function setupWithNoUsername() {
const utils = setup()
utils.changePasswordInput(utils.user.password)
utils.clickSubmit()
const errorMessage = utils.getByRole('alert')
return {...utils, errorMessage}
}
Not everyone is convinced
from pytest import fixture
@fixture
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
@fixture(scope="session")
def dynamodb():
container, endpoint_url = start_dynamodb_local()
yield SimpleNamespace(container=container, endpoint_url=endpoint_url)
container.stop()
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
def test_traffic_split(dynamodb):
traffic_split = TrafficSplitDb(endpoint_url=dynamodb.endpoint_url)
...
def test_email_campaign(smtp_connection, dynamodb):
campaign = CampaignManager(smtp_connection, dynamodb)
...