Let us take a simple TodoMVC application, from cypress-example-todomvc for example, and add a single Cypress end-to-end test like this one
1 | beforeEach(() => { |
The test passes.
But what did this test cover? Code coverage is only useful for unit tests. In the final web application, the code reachable from the user interface is probably a very small part of the total bundle; plus there is a lot of vendor code and polyfill libraries - all rendering 100% code coverage a mirage.
What can we do instead of code coverage? Well, looking at the application the page elements are a natural candidate, isn't it? Did our test type into the input field? Yes, it did! Did the test click on the check box to mark a todo item completed? No, our test did not do that. Our tests also did not click on the filters "All, Active, Completed" at the bottom.
It would be nice to show the elements covered by the tests, so we can easily see how to extend a test, or maybe write another one, in order to cover the entire application's user interface.
If you are using Cypress like I do here, this is a cool experiment to try. You can find the code I am about to write in a branch mark-touched-dom-elements of cypress-example-todomvc.
First, let us collect all elements the test types into using cy.type
command. We can detect the type command by overwriting it.
1 | Cypress.Commands.overwrite('type', function (type, $el, text, options) { |
The same test now prints the selector .new-todo
twice in the DevTools console.
The jQuery property .selector
has been deprecated, and is not very useful. Instead I found NPM package @medv/finder that is well-tested and produces quite nice selectors even for deeply nested elements. Let's use this module, and also let us collect all selectors we see.
1 | const finder = require('@medv/finder').default |
Now that we have collected a list of selectors, we should do something after all tests finish. For example, we could highlight all those elements with a nice magenta border.
1 | after(() => { |
The test runs again, and we see the input element highlighted.
Let us mark one item completed and see it. We are going to use cy.check
command in the test, and we will overwrite it in our cypress/support/commands.js
file
1 | // cypress/integration/add_spec.js |
The found selector "li:nth-child(1) .toggle" is not the most attractive one, but if we want a better selector we should set a data attribute on the element in our render
function, right?
Finally, let us cover the filters at the bottom, since we now have both completed and unfinished items in the list.
1 | // cypress/integration/add_spec.js |
Now a lot of visual elements are covered by the test
Even selectors look nice, because I have added attributes in footer.jsx
1 | <a |
Using data-*
attributes for testing is our recommended best practice when selecting elements.
Limitations
While seeing the elements covered by the test is nice, and you can even take a screenshot at the end using cy.screenshot
, it is of limited utility. Because while it marks an element the test has acted upon (typed into, checked, clicked), it does not show what kind of interaction it was, or even if the interaction has covered all possible interactions. For example, our test checked an checkbox element, and we see this, our test has NOT unchecked it. To me, this screams to require state coverage, rather than element coverage.