Juliano
Posted on December 23, 2019
Some Initial thoughts
One important (let's avoid the word "essential" here) element to keep a long-living application maintainable is the presence of tests. It's a common-sense that they are a must-have piece of any healthy codebase and come in some flavors: Unit, Integration, and Functional (end-to-end or e2e) tests. You can read more about each one on this great article. There are two kinds of elements to be tested: ui and non-ui components (in the former case, you can perform visual and/or non-visual tests). The adoption of one or more types of tests and the corresponding tools that will make it possible to run them automatically depends on the project and its natural constraints (team expertise, team size, deadlines, adopted technologies, presence or absence of CI/CD, etc).
In JavaScript world, like in other languages, we have different tools for each type of test (unit, integration, e2e).
A few words about unit tests
As of the writing of this post, we typically use Jest/Karma to perform unit testing (static, isolated, and integration) on non-ui components: data models, data services, facades, state management, etc. And - with Angular - we can use the TestBed
object, provided by the Angular framework, to configure Dependency Injection (DI) and ngModules
within a test cycle.
On the other hand, we have the harsh reality: UI components unit testing is often problematic, painful, and provides little true ROI. It's not rare to have more lines of testing code in the UI specs files than the UI component code under test itself. But this doesn't mean we don't want to test them. They should be tested as everything else in your code, maybe not at unit level though.
Cypress.io and e2e testing
We often want to test UI components to answer two basic questions:
(1) Does the UI render as we expected; not just DOM structure but also the styling and layouts?
(2) Do the workflows perform as expected?
Cypress.io elegantly provides powerful, intuitive features to do these.
Cypress is a great agnostic javascript testing tool that shines on e2e tests, as an alternative to Selenium (wrapped by Protractor in Angular framework). For aspect (2) above, Cypress.io simulates the user and the 'user' interactions based on custom Cypress scripts (eg steps).
The most exciting aspect is that Cypress.io has brought an incredibly friendly API to the JavaScript testing world. If you're a complete newbie to Cypress.io, it probably won't take more than 10 minutes to go from scratch to having a simple working test. Its documentation is amazingly easy to read with lots of examples.
If we want to use Cypress to test UI component functionality and UI, we do not need a TestBed
... we are testing the actual "deployed" Application (instead of only component source) and all the necessary elements to run the tests are already instantiated.
Among all of e2e aspects concerning the adoption of Cypress.io for an Angular codebase, we'll focus on how we can spy or stub an Angular Component method without having a TestBed
, by grabbing the available component running instance from the DOM.
Why stub or spy?
During unit or integration tests, you usually search for elements on the DOM, like texts, components attributes, etc, to check if the app is showing/hiding or animating anything. According to what you find on the DOM you get to the conclusion that a piece of the web app is working and the users supposedly see on the screen what you expected them to see. For example, if you find that a certain ui component has a "display: none" attribute, you should trust that it won't be visible or taking any space on the screen. You write your tests based on the trust that the browser is a piece of this puzzle that is already tested by its developers and is doing its job correctly. In other words, you're focused on your code only, so you can write your tests using non-visual (functional) isolated tests.
If you are concerned about how the browser is doing its job (visually) or about whether your CSS classes weren't broken by someone messing around with the design then you'll need some testing tools to compare snapshots of the current browser's rendering results with some known reference snapshots. We are aware that each browser renders things differently from each other. You can add plugins to Cypress.io to test your application design visually.
But if you only want to keep track of the application workflows during an e2e testing cycle, Cypress.io can provide you enough tools out-of-the-box (no need for plugins). Sometimes, inspecting what is inside of a component is just a hard thing to do. For example, if you're using chart.js, it uses canvas
to draw the charts - it's not possible to check the canvas content from a functional approach. Instead of trying to inspect what is inside the canvas
element you'd better step back and just stick to checking, for example, if the function that generates the data for the chart was successfully called with the right arguments; or whether the event handlers that should be fired as a consequence of user interactions with the canvas elements were really called. In that case, when monitoring these function callings, you want to know three things:
(a) Whether and (b) when a function is being called
(c) Whether it's being called with the right arguments
To do (a) to (c) you must get a reference to your angular component object (the instance of the component's class). For us it's important to keep in mind that we don't have the TestBed
available in this scenario, so we have to grab the component instance object by ourselves.
Let's do it
Let's suppose you have the following ComponentUnderTest
:
@Component(
selector: 'comp-to-test',
template: `
<button (click)="_methodToBeTested($event)"
cy-data-button>
Click me
</button>
`
)
export class ComponentUnderTest {
// This method is fired upon user interaction
_methodToBeTested(...) { ... }
}
To gain access to the methodToBeTested
we'll use the ng
global object available from the global window
object. From that point on it's simple to do what you need.
The important thing to notice is that you cannot access window
or document
directly from within your test it()
function. You must use the available cy.window()
and cy.document()
chainable methods to access those objects.
Using cy.stub
Your test will probably look like this:
describe('Trying to call some angular component method'), () => {
it('Should call methodToBeTested' => {
let angular!: any;
// You can access the window object in cypress using
// window() method
cy.window()
.then((win) => {
// Grab a reference to the global ng object
angular = (win as any).ng;
})
.then(() => cy.document())
.then((doc) =>{
const componentInstance = angular
.getComponent(doc.querySelector('comp-to-test'));
cy.stub(componentInstance, '_methodToBeTested');
cy.get('button[cy-data-button]').click();
// Just put this test to the end of the event loop
// in order to make sure angular runtime engine
// will have fired the click event that calls the
// method calling under test.
cy.wait(0).then(() =>
expect(componentInstance._methodToBeTested).to.have.been.called);
});
});
});
Using cy.spy
Now, the stub approach is fast. But it can potentially hide the side effects that clicking on that button can cause. In the end, by using Cypress, I assume that you're interested in e2e tests, and chances are that you want to observe the thorough behavior of your app. If you wish to through the real side effects caused by the method called when that button is clicked, you must use cy.spy
instead of cy.stub
. If anything fails you will be able to visually inspect what happened during the test (by the way, Cypress, by default, records all of the application visual behavior, even in headless mode, in mp4 format).
So we need to do a single adjustment to replace cy.stub
:
describe('Trying to call some angular component method'), () => {
it('Should call methodToBeTested' => {
let angular!: any;
cy.window()
.then((win) => angular = (win as any).ng)
.then(() => cy.document())
.then((doc) =>{
const componentInstance = angular
.getComponent(doc.querySelector('comp-to-test'));
// Here we had cy.stub before
cy.spy(componentInstance, '_methodToBeTested');
cy.get('button[cy-data-button]').click();
cy.wait(0).then(() =>
expect(componentInstance._methodToBeTested).to.have.been.called);
});
});
});
Conclusion
If you're thinking of sparing some time to learn JavaScript testing tools, it's worth it taking a look at Cypress.io. It's a short-learning-curve and still powerful tool available out there.
If you find something that I left out from this article, in the scope of stubs and spies with Angular, just mention it in the comments and I'll insert it in the text.
Also, I owe a special thanks to Thomas Burleson for dedicating some time to review the text and point some directions concerning the reasons that support Cypress.io integration to angular.
Posted on December 23, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.