Lately I have been looking for alternatives for the 'commum' way of writing automated tests, the automated tests code in my view, care too much with code, and little with behavior.
To change this, I found the Screenplay pattern, where, basically, we will write test code thinking on behavior, results, and the code will only be a tool to this.
The Screenplay focus on Actors, this Actors have Abilities, make Actions and asks Questions, with this, we can abstract our code to be outside the test.
I implemented the Screenplay pattern on Cypress and Typescript like this, for a software of warehouse management:
Let's imaginate a warehouse management system, in this system, we have two types of users, the managers and the workers.
The workers, receive new products, make purchase orders and label new products, the managers, approve purchase orders, generate reportes and more.
Let's see how can we automate the tests for this software using Screenplay
PS: I will simplify the code for a better understanding
Actors:
1. I use a base-actor:
- in the base-actor has a constructor, that receives a list of actions and questions
- there is the methods
makeAction
andmakeQuestion
### 2. Each user of tests extends this base-actor passing to him necessary Actions and Questions:
actors/warehouseManager.ts
WarehouseManager{
actions:
approvePurchaseOrder,
generateReports,
planShipments,
assignTasks,
createProducts
questions:
wasPurchaseOrderApproved,
wasReportGenerated,
wasShipmentPlanned
wasTaskAssigned,
wasProductCreated
}
actors/warehouseWorker.ts
WarehouseWorker{
actions:
makePurchaseOrder,
receivesDeliveries,
generateProductLabels
questions:
wasPurchaseOrderGenerated,
wasDeliveryReceived,
wasProductLabelsGenerated
}
2. Actions:
- Actor make Action to interact with the system
- I divide Actions based on features
- Actions can receive parameters
- Actions describe a behavior of user on the feature
actions/purchaseOrder.ts
purchaseOrderActions{
makePurchaseOrder(input: NewPurchaseOrderInput){
cy.get('#btn-new-purchase-order').click()
for(const item of input.items){
cy.get('#order-new-item').click()
cy.get('#input-search-items').type(item)
cy.get('#select-option').contain(item)
}
...continues
}
approvePurchaseOrder(input: ApprovePurchaseOrderInput){
cy.get(`#purchase-order-${input.orderId}`)
cy.get('#btn-approve-order').click()
}
}
actions/products.ts
productActions{
createProducts(input: NewProductInput){
cy.get('#btn-new-product').click()
cy.get('#input-product-name').type(input.productName)
...continues
}
generateProductLabels(input: GenerateProductLabelInput){
cy.get('#btn-generate-product-label').click()
cy.get('#select-label-type').click()
cy.get(`#option-${input.labelType}`).click()
for(const product of input.products){
cy.get('#select-products').click()
cy.get(`#option-${product}`).click()
}
...continues
}
}
3. Questions:
- Actor make Questions to validate system state
- Questions are also based on features
- Questions can receive parameters
- A Question is a 'expected' on the end of code
questions/purchaseOrder.ts
purchaseOrderQuestions{
wasPurchaseOrderApproved(input: WasPurchasedOrderApprovedInput){
cy.get(`#orders-li-${input.orderId}).click()
cy.get('#order-status').should('be.eql', input.expectedStatus);
}
wasPurchaseOrderGenerated(input: WasPurchaseOrderGeneratedInput){
cy.get(`#orders-li-${input.orderId}).should('be.visible')
}
}
questions/products.ts
productQuestions{
wasProductCreated(input: WasProductCreatedInput){
cy.get('#li-products').should('contain', input.productName)
...continues
}
wasProductLabelsGenerated(input: WasProductLabelsGeneratedInput){
cy.get('#btn-product-labels').click()
for(const product of input.products){
cy.get(#li-product).should('contain', product)
}
...continues
}
}
With this design it's easy to write test code
For example:
e2e/cypress/purchaseOrders/approvePurchaseOrder.ts
describe('approval of purchase orders', () => {
beforeEach(() => {
const databaseAdministrator = new DatabaseAdministrator();
databaseAdministrator.makeAction('generateData', seed);
})
it('warehouse manager should approve a purchase order that is pending', () =>{
warehouseManager = new WarehouseManager()
warehouseManager.makeAction('visitScreen', 'orders');
warehouseManager.makeAction('approvePurchaseOrder', 'new-order-01');
warehouseManager.makeQuestion('wasPurchaseOrderApproved', 'new-order-01')
})
})
e2e/cypress/products/generateProductLabels.ts
describe('generate product labels', () => {
beforeEach(() => {
...handle test preparation
})
it('warehouse worker should generate product labels', () =>{
warehouseWorker = new WarehouseWorker()
warehouseWorker.makeAction('visitScreen', 'products');
warehouseWorker.makeAction('generateProductLabels', ['item-01', 'item-19', 'item-2523']);
warehouseWorker.makeQuestion('wasProductLabelsGenerated', ['item-01', 'item-19', 'item-2523'])
})
})
- remember, the code it's simplified to better understanding, real code need more details
Key Points:
1. With this pattern, we create tests that are easy to understand and maintain.
- Tests written in the screenplay pattern use a structured, human-readable format, reducing the learning curve for new team members.
- The separation of concerns in this pattern minimizes redundancy and simplifies updates when the system changes.
2. Writing frontend code with good test identifiers facilitates the creation of automated tests.
- Well-named selectors and identifiers make it easier to map test steps to system functionality, improving readability.
- By merely reviewing the Actions in the tests, stakeholders can grasp how the system behaves without needing in-depth technical knowledge.
3. Instead of using Cypress env to manage commands, we use Classes, like common code.
- This approach aligns test code structure with the development team's patterns, fostering consistency across the project.
- Using classes promotes reusability and makes the test code more modular and scalable.
4. Inside Actions, there could be other commands, like 'selectOption' or 'visitScreen'.
- For smaller, reusable commands, Cypress.Commands.add is a practical choice to encapsulate these behaviors.
- This ensures that the primary logic in Actions remains clean and focused, while Cypress env handles auxiliary functionalities.
Author Of article : Will Drygla Read full article