I have rewritten my tests again and this time I have used app actions. My tests are using Cypress commands like cy.visit
again (highlight cy.visit
in beforeEach
) without going through an intermediate object.
Here are my rules for app actions. If a group of tests like "New Todo" (expanda "New Todo" tests) are exercising a particular feature on the page, like the input element to add todos, then these tests are going through normal DOM selectors and actions, just like a real user would.
highlight const NEW_TODO = '.new-todo'
selector.
expand test "should allow me to add todo items"
See how this test for adding new todo, goes through the page?
But other tests, that exercise other features, like Routing tests are different
scroll to and expand "Routing" tests
These tests are NOT interested in testing adding new todo items - this feature works, we know it, because our "New Todos" tests are passing. So the routing tests instead of using the DOM and typing new items again and again, can just invoke a method on the model instance and creat the todos to work with.
highlight beforeEach(addDefaultTodos)
In this beforeEach
we are calling a utility function addDefaultTodos
that I have defined in utils.js file.
switch to util.js file
_ highlight addDefaultTodos
function_
See how this function gets the application's window
context, grabs its property called model
, and invokes method addTodo
with 3 arguments? This instantly creates 3 todos, just like we did from DevTools console.
go back to spec.js file
set .only on test "should allow me to display active items"
Let's look at the first routing test here. We have created the initial 3 items using app action. Then we call toggle
(highlight toggle(1)
) which is also an app action - because the toggle through the page is actually tested by other tests. So routing should not retest the same feature.
But the clickFilter('Active')
is a normal function that does go through the page and does click on the link in the DOM using Cypress commands. Because this is the page feature that this test is actually testing - it should not do the app action in this case.
save the spec file and switch to Cypress
rerun the test
The test created 3 items using an app action (highlight the INVOKE
command), then toggled the middle one using an app action (highlight the toggled log message
). Then it went through the DOM and clicked on the "Active" link and asserted that only the first and the third todo items are shown.
And notice that the test was fast (rerun the test), because it did not have to type 3 items, or click toggle on the page. App actions go directly into the application, bypassing the DOM. This makes tests run much faster.
switch to spec.js and remove .only
and save the file
switch to Cypress
The tests now use mostly app actions to set the application's state before testing a particular feature, and this makes the tests run twice faster than the original page tests.
Besides speed and removing an entirely unnecessary level of abstraction compared to page objects, app actions have one other huge benefit.
switch to spec.js
Instead of building my test helpers on top of HTML and selectors that are not linted or statically checked, I am working directly against the application's code. This means that my JavaScript spec is calling into my app's JavaScript. And even in JavaScript in a modern code editor like VSCode, I can add this comment ts-check
(highlight // @ts-check
comment) and VSCode will check my test commands against what my app model can do!
For example, let me grab the window
object, then its
property model
. What methods can I call on the model
from my tests?
type
cy.window()
.its('model')
.then(model => {
model.addTodo('first', 'second')
})
and show intellisense after model.
IntelliSense is showing me the app actions I can call. And if I try to use an incorrect method, like window.foo
(type window.foo
or window.addTodos
) TS check will warn me. It even will understand the types of the arguments, like window.addTodo(1, 2, 3)
should be window.addTodo('first', 'second')
.
How does this work if both my app and my spec are plain JavaScript files. I have added reference path
special comment to the spec file (highlight reference path=...
comment). The TypeScript file model.d.ts
is something I wrote. (click on model.d.ts
top open it). It describes the interface for the model instance and adds property model
to the global window
object.
If I had written my TodoMVC app in TypeScript in the first place, I would not have to do this, but still, having an interface like this that ts-check
can statically check against, gives me huge confidence in my tests. And the TodoModel class is a lot more stable and less likely to change than the page HTML.