157 lines
4.9 KiB
Markdown
157 lines
4.9 KiB
Markdown
---
|
|
title: "Jest parameterization"
|
|
slug: /jest-parameterization/
|
|
date: 2023-11-20
|
|
tags: ["learning", "javascript", "unit-testing"]
|
|
---
|
|
|
|
At work I am in the process of upgrading our AWS Lambdas to use the v.18 Node
|
|
runtime (most of them are still using v.14). It has been a good opportunity to
|
|
carry out refactoring and address technical debt.
|
|
|
|
Many of the lambdas lack unit tests whilst others have tests that haven't been
|
|
maintained. Thus I've been spending time adding and optimising tests in Jest.
|
|
|
|
I've been harnessing parameterization when improving unit test coverage. The
|
|
lambdas I'm currently working on are subroutines within a broader
|
|
[AWS Step Function](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html)
|
|
that constitutes the backend of one of our internal content management systems.
|
|
Much of the functionality consists in generating and parsing properties from XML
|
|
and JSON. As I am testing the same code under different conditions, the tests
|
|
are highly repetitive and can be readily parameterized.
|
|
|
|
Here's one example:
|
|
|
|
```js
|
|
describe("handler", () => {
|
|
let mockApiGatewayEvent = {
|
|
propertyId: "1234",
|
|
isCrossPublished: false,
|
|
}
|
|
describe("exit conditions", () => {
|
|
it("should throw an error if a user attempts to unpublish an alpha file", async () => {
|
|
event = {
|
|
...event,
|
|
fileId: "alpha:1234",
|
|
}
|
|
await expect(handler(event)).rejects.toThrow(
|
|
"Not allowed to unpublish file alpha:1234"
|
|
)
|
|
})
|
|
it("should throw an error if user attempts to unpublish a beta file", async () => {
|
|
event = {
|
|
...event,
|
|
fileId: "beta:1234",
|
|
}
|
|
await expect(handler(event)).rejects.toThrow(
|
|
"Not allowed to unpublish file beta:1234"
|
|
)
|
|
})
|
|
// and so on...
|
|
})
|
|
})
|
|
```
|
|
|
|
I've anonymised the specifics of the data but the process is straightforward:
|
|
I'm asserting that the correct error text is returned if a user attempts to
|
|
delete a certain filetype. I reduced the verbiage of countless `it` blocks by
|
|
utilising parameterization:
|
|
|
|
```js
|
|
describe("exit conditions", () => {
|
|
let mockApiGatewayEvent = {
|
|
projectId: "1234",
|
|
preview: false,
|
|
}
|
|
|
|
it.each([["alpha:1234"], ["beta:1234"]])(
|
|
"should throw an error if user attempts to unpublish %s file",
|
|
async (fileId) => {
|
|
mockApiGatewayEvent = {
|
|
...mockApiGatewayEvent,
|
|
fileId,
|
|
}
|
|
await expect(handler(mockApiGatewayEvent)).rejects.toThrow(
|
|
`Not allowed to unpublish file ${fileId}`
|
|
)
|
|
}
|
|
)
|
|
})
|
|
```
|
|
|
|
Instead of multiple `it` clauses, there is a single `each` expression that loops
|
|
through each file variant, executing the same test each time, changing only the
|
|
file name that is output.
|
|
|
|
The `%s` symbol is a placeholder for string substitution. This lets me include
|
|
the name of each variant in the test description. This is important because
|
|
there is a risk of obscuring the specifics of each test iteration when using
|
|
parameterization. It's essential to be able to trace a failure to the specific
|
|
iteration.
|
|
|
|
In the example below, the process is functionally the same but there are more
|
|
parameters in the mix:
|
|
|
|
```js
|
|
describe("deletePageFromS3()", () => {
|
|
beforeEach(() => {
|
|
process.env.AWS_ENV = "live"
|
|
s3ClientMock.reset()
|
|
})
|
|
|
|
const parameters = [
|
|
{
|
|
previouslyPublished: false,
|
|
isDraft: false,
|
|
bucket: "bucket:alpha",
|
|
key: "key:alpha",
|
|
},
|
|
{
|
|
previouslyPublished: true,
|
|
isDraft: false,
|
|
bucket: "bucket:beta",
|
|
key: "key:beta",
|
|
},
|
|
{
|
|
previouslyPublished: false,
|
|
isDraft: true,
|
|
bucket: "bucket:gamma",
|
|
key: "key:gamma",
|
|
},
|
|
{
|
|
previouslyPublished: true,
|
|
isDraft: true,
|
|
bucket: "bucket:delta",
|
|
key: "key:delta",
|
|
},
|
|
]
|
|
|
|
it.each(parameters)(
|
|
"should return page for deletion, given: previouslyPublished is %s, isDraft is %s",
|
|
async ({ previouslyPublished, isDraft, bucket, key }) => {
|
|
await deleteFileFromS3("url", previouslyPublished, isDraft)
|
|
const deleteObjectCommand = s3ClientMock.calls()[0].args[0]
|
|
expect(deleteObjectCommand.input).toEqual({
|
|
Bucket: bucket,
|
|
Key: key,
|
|
})
|
|
}
|
|
)
|
|
})
|
|
```
|
|
|
|
The process under test is a function that uses the AWS SDK to delete objects
|
|
from an S3 bucket. I am checking that the (mocked) S3 client is called with the
|
|
correct parameters.
|
|
|
|
This time I am passing in an array of objects to the `each` function rather than
|
|
a multi-dimensional array. This makes it easier to destructure the specific
|
|
properties in the individual test cases. Again I use `%s` to interpolate a
|
|
subset of the parameters into each test description. `%s` applies to the each
|
|
value in the `each` array in sequence so you just repeat it to individuate the
|
|
different params.
|
|
|
|
This has been a brief sketch of some applied examples of parameterization. For a
|
|
better account, see
|
|
[Parameterized tests in JavaScript with Jest](https://blog.codeleak.pl/2021/12/parameterized-tests-with-jest.html)
|
|
by Rafał Borowiec.
|