loading...

Accessing angular components methods from within Cypress

julianobrasil profile image Juliano Updated on ・6 min read

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

Typically we 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 for two aspects:

(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 testing UI component functionality and UI, we do not use 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 component running available 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 user supposedly sees on the screen what you expected heshe to see. For example, if you find that a ui component has a "display: none" attribute, you 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 and is doing its job correctly. In other words, you're focused on your code only, so you write your tests using non-visual (functional) isolated (at some level) 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 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 with 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 two things at most:

(a) Whether and when a function is being called
(b) Whether it's being called with the right arguments

To do (a) and (b) you must get a reference to your angular component object (the instance of the component's class) - remember that you don't have the TestBed in this scenario, so you have to grab your component instance object by yourself.

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 withing 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:

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 keep the 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 worth take 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 sparing some time to review the text and point some directions concerning the reasons that support Cypress.io integration to angular.

Posted on by:

julianobrasil profile

Juliano

@julianobrasil

I'm a full-stack web developer, passionate about all kinds of technologies. Formerly bachelor and MSc in Electrical Engineering.

Discussion

markdown guide