It sounds like the beginning of a joke, but it’s no joke! If you’re not convinced, just ask Keanu Reeves.
ACT 1: EXPOSITION
A lot has been written about the all-mighty constant Phi (φ = 1.6180339887…), also known as the Golden Ratio or the Divine Proportion! It’s the mystic number that represents perfect beauty and keeps showing up everywhere in the known universe.
What about cheese pizza? The cheesi-est, umami-est, and yummi-est of all pizzas in the known universe (if you don’t believe it, just ask any kid).
Disclaimer: I will not delve into the well-known discord over whether that would be a New York cheese pizza or a Chicago cheese pizza; that’s a topic for another day and a different blog.
But do not forget the third pal of these ‘tres amigos’: the Cypress fixture, which stores our most precious test data for our most valuable automation test scripts, also in the whole known universe.
What if we tamper with our divine Phi by changing just one of its digits? For example, if we change the first decimal from 6 to 3, the result is 1.3180339887… We’ve just diminished its luster. It is no longer golden neither divine, and has become a mere mundane decimal number like any other.
What might be an even worse profanity… Take your yummy cheese pizza and add Canadian bacon and pineapple. What you now have is a Hawaiian pizza! Which to many is probably the greatest blasphemy one can commit in the culinary pizza world. You can pull the pineapple, but it is already ruined, it will taste to pineapple for the rest of the eternity.
Another disclaimer: Hey, these are not my words, but my kids’. But who am I to argue with them?
Now, let’s semantically analyze the word ‘fixture’ within the context of Cypress fixtures. A fixture is defined as a piece of equipment that is fixed in position. According to Cypress.io documentation, it refers to a fixed set of data located in a file. So, hypothetically speaking, what if we were to change the value of the fixture after loading from the file during our tests? Would it not cease being a fixture and become a variable instead?
ACT 2: CONFRONTATION
There are things in the known universe that we must not (or at least, should not) mess up with! There are things that must be (or at least, should be) immutable: Phi, a cheese pizza, and definitely a Cypress fixture!
Because if a Cypress fixture is mutable, not only might your test be erroneous, but your tests could also overlook a major security issue in your system under test!
Our Application
You have a backend application that ensures only authorized requests can cancel an existing contract for undertaking a job. There is an API endpoint, /contracts/cancel
, that will cancel the contract if it receives the correct contract number, purchaser’s name, price rate, beneficiary of the job, and one of the two: authorized phone or authorized email.
Our tests will need to verify that the backend logic is implemented correctly and that none of the validations are overlooked.
The record in the contracts table in the database is as follows:
contractNumber = "11111"
jobDescription = "Hawaiian pizza slice"
purchaser = "Winston Scott"
contractor = "Caine"
beneficiary = "John Wick"
goldCoinsPriceRate = 3500
authorizedPhone = "310-564-8005"
authorizedEmail = "winston.scott@thehightable.com"
The cancellation service API is a POST
method for the endpoint /contracts/cancel
:
And the query the backend developer implemented to retrieve a contract record based on the API request is:
SELECT * FROM contract
WHERE contractNumber = @contractNumber
AND purchaser = @purchaser
AND beneficiary = @beneficiary
AND goldCoinsPriceRate = @goldCoinsPriceRate
AND authorizedPhone = @authorizedPhone
If the query finds a record that matches the selection, the API will grant the cancellation by returning {cancelationSuccess: true}
; otherwise, it will return {cancelationSuccess: false}
.
Notice that the developer forgot to include the authorizedEmail in the database query, introducing a defect in the application, right?
The query should have been instead as follows:
SELECT * FROM contract
WHERE contractNumber = @contractNumber
AND purchaser = @purchaser
AND beneficiary = @beneficiary
AND goldCoinsPriceRate = @goldCoinsPriceRate
AND (authorizedPhone = @authorizedPhone OR authorizedEmail = @authorizedEmail)
Our Tests
For this purpose, you create a fixture that you plan to reuse in many of your tests, containing a set of valid data for some of the fields you need to validate.
Our fixture file is located at cypress/fixtures/contract.js
:
{
"contractNumber": "11111",
"purchaser": "Winston Scott",
"goldCoinsPriceRate": 3500,
"beneficiary": "John Wick"
}
And this is how we implemented our test suite cypress/e2e/contract-cancel.js:
/// <reference types="cypress" />
import contract from '/cypress/fixtures/contract.js'
describe('Test API /contract/cancel', () => {
it('Test cancelation with Authorized Phone only', () => {
contract.authorizedPhone = "310-564-8005"
cy.request({
method: 'POST',
url: '/contract/cancel',
body: contract,
})
// Note - contract object sent in the request contains:
// {
// "contractNumber": "11111",
// "purchaser": "Winston Scott",
// "goldCoinsPriceRate": 3500,
// "beneficiary": "John Wick",
// "authorizedPhone": "310-564-8005"
// }
});
it('Test cancelation with Authorized Email only', () => {
contract.authorizedEmail = "winston.scott@thehightable.com"
cy.request({
method: 'POST',
url: '/contract/cancel',
body: contract,
})
// Note - contract object sent in the request ACTUALLY contains:
// {
// "contractNumber": "11111",
// "purchaser": "Winston Scott",
// "goldCoinsPriceRate": 3500,
// "beneficiary": "John Wick",
// "authorizedPhone": "310-564-8005",
// "authorizedEmail": "winston.scott@thehightable.com"
// }
// The property "authorizedPhone" still remains from the previous test!!!
});
});
The first test (‘Test cancelation with Authorized Phone only’), will pass, because the SQL in the backend will find a record for the fields provided in the request body: all valid fields from the fixture, as well as a valid authorizedPhone
, according to the stored database record.
If you pay attention, you will notice that the tester actually modified the imported fixture directly by executing the statement
contract.authorizedPhone = "310-564-8005"
on the global objectcontrac
t.
For the second test (‘Test cancelation with Authorized Email only’), the tester provided an authorizedEmail
according to the database record, so they expect the test to pass, verifying that the API service is implemented as expected.
And the test indeed pass. However, the real reason the second test passed is that the tester had changed the originally imported fixture in the first test. As a result, for the second test, the contract object still contains contract.authorizedPhone = "310-564-8005"
, thereby breaking the principle of test independence!
But, since all tests passed… LET’S DEPLOY TO PRODUCTION!!!
And this is when all the troubles begin!
Later, when Winston tries to cancel the contract targeting John using his authorized email (cannot use his authorized phone because he forgot the phone while having a dirty martini with his pals at The High Table), his /contracts/cancel
request will not be successful, and the contract to take care of John is not cancelled.
And we all know what happens when someone tries to go after John, our Divine Proportion (and one of the most formidable forces in the known universe)…
Author’s note: By the way, Keanu Reeves thinks pineapple belongs on pizza — but only under one specific condition: “if the pineapple is slightly roasted”. So I may need to reconsider my Hawaiian pizza statement at the beginning of the post. 😉
ACT3: RESOLUTION
Let’s then keep Cypress fixtures immutable, but how can we do that?
We can simply copy the original fixture and then add whatever additional properties we might need to the copy. The easiest way to do this is by using the spread operator ...
, which was introduced in JavaScript in the ES6 version.
The line below will create a new object, contractToCancel
, that is a copy of the original global object contract
, with an added property authorizedPhone
:
const contractToCancel = {...contract, authorizedPhone: "310-564-8005"}
So our original spec code would look like something like:
/// <reference types="cypress" />
import contract from '/cypress/fixtures/contract.js'
describe('Test API /contract/cancel', () => {
it('Test cancelation with Authorized Phone only', () => {
const contractToCancel = { ...contract, authorizedPhone: "310-564-8005" }
cy.request({
method: 'POST',
url: '/contract/cancel',
body: contractToCancel,
})
// Note - contractToCancel object sent in the request contains:
// {
// "contractNumber": "11111",
// "purchaser": "Winston Scott",
// "goldCoinsPriceRate": 3500,
// "beneficiary": "John Wick",
// "authorizedPhone": "310-564-8005"
// }
});
it('Test cancelation with Authorized Email only', () => {
const contractToCancel = { ...contract, authorizedEmail: "winston.scott@thehightable.com" }
cy.request({
method: 'POST',
url: '/contract/cancel',
body: contractToCancel,
})
// Note - contractToCancel object sent in the request contains:
// {
// "contractNumber": "11111",
// "purchaser": "Winston Scott",
// "goldCoinsPriceRate": 3500,
// "beneficiary": "John Wick",
// "authorizedEmail": "winston.scott@thehightable.com"
// }
//
});
});
Since the copies of contractToCancel are defined within the context of each test, they will not affect other tests because they are destroyed after the test is completed, leaving our original fixture in the global object contract untouched.
One last thing
What if you want to remove a property from the original fixture while keeping the original object immutable?
For that, we can use object destructuring, also introduced in ES6.
The line below will create a new object, newContract
, that is a copy of the original global object contract
, with the property beneficiary
removed:
const { beneficiary, newContract } = contract
Now you know: do not mess with Cypress fixtures; they are intended to be just that, fixed assets. Otherwise, you know what pure mayhem could ensue in your perfectly beautiful and balanced known universe (AKA tests).
Don't forget to leave a comment, give a thumbs up, or follow my Cypress blog if you found this post useful.
Happy reading!
Top comments (0)