Overview
In the previous article, I defined “Unit Testing” and how we can apply those tests to our Angular application. We agree that, by definition, our business logic should live outside of our components, and most of our functionality will live in pipes, directives, and services, meaning most of our unit tests will test those types of classes. In that article, I also posted a gist of an Angular component with its templating logic moved from the component into a pipe, leaving only an Input() in the class definition and some templating. We would all agree that, at any point, someone could go in and add or remove elements in the element, and there are no amount of unit tests that could capture those changes.
So, if you can’t use unit tests to test a component properly, you’re only left with either component, integration, or end-to-end testing.
Integration Testing could work, but by definition, integration testing occurs when you test individual units and combine them to test as a group. So, for example, you could test ComponentA with ComponentB, and that’s probably fine in some cases. But what happens when you integrate ComponentA with ComponentC? Are you comfortable saying that the first test you created is good enough to ship your product to production?
According to the Angular docs:
The basic building blocks of the Angular framework are Angular components…
Components are designed to be combined with and consumed by different components to build your application. Therefore, you need to be able to ensure that you have some source of truth for the DOM, specifically your accessibility features, Input and Output properties, and any styling you feel pertinent to make sure it is always valid and that you wouldn’t need to test manually.
Let’s go back to the Header component:
https://medium.com/media/4b80f2ea6da5acb6035bb4009ba842c9/href
At first glance, there isn’t much to test. In an ideal world, we would know that the NameDisplayPipe has tests that validate the values provided by the component and return what we expect. But what happens if someone accidentally adjusts the h1 to an h2? What if someone accidentally removes the initialization of the title?
Component Testing to the Rescue
Component testing is incredible since it removes the need to manually test many things we’ve grown accustomed to testing manually. For example, I only know a few developers that think to define the structure of the component’s template (the most crucial part) through tests; however, I do know many QAs and designers that manually check these things. Yet, it’s something that we don’t generally discuss. We are adamant that we test our functionality and behaviors, but we are only keen on testing templates once the code is in production. It’s simply an afterthought.
Assume I’m given a mock of a page with a header, a body, and a footer. For the sake of this article, the contents of the body don’t matter, but as one work item, I am to do all three things in one sprint. So there would be at least four components: a HeaderComponent, BodyComponent, FooterComponent, and a PageComponent that integrates the other three…
https://medium.com/media/5cf7af3d706cacb98c47649354dd4d0a/href
We have already created the header component, but let’s pretend we didn’t. If we have mockups of the header, we could define what would need to always be true in this component:
The component should wrap its contents in a header element.By default, if a user isn’t logged in, the header should say: “Hello, User” in an h1.If a user is logged in, the header should say: “Hello, {{ User Name }}” in an h1.
We could even get crazy and test the font size, color, or other styles or accessibility points we could come up with, but for this example, we’ll test against these three items.
For component testing, you could use Jasmine, Jest, or Cypress. With Jasmine and Jest, you’re reliant on TestBed and using a fixture to grab elements and assert on them. With Jest, you don’t see the template in an actual DOM, so it almost gives no real benefit. Jasmine works great here; out of the box, it does nearly everything you want it to do. Namely, being able to time-walk through the tests and look at snapshots of your tests. As a Cypress Ambassador, you probably guessed that I prefer to use Cypress for this, and as of Cypress 10.5 we can make that happen!
TDD With Cypress Component Testing
Cypress component testing has a very similar syntax as Cypress End-to-End testing. If you know how to write a “Cypress Test,” you know how to write a component test. Using the 3 test cases I created above, I can quickly structure a spec file of tests that relate to each of them:
https://medium.com/media/051e2116714bbed65221e45654688e24/href
If you’ve seen a Cypress test in the past, this should look pretty familiar. If you haven’t, let’s take a look at each piece:
It should be within an header element:
it(‘should contain a header’, () => {
cy.mount(HeaderComponent);
cy.get(‘header’)
.should(‘exist’)
.should(‘be.visible’);
});
Just like Jasmine, Jest, or any other testing frameworks before them, Cypress specs are wrapped in describe (or context)blocks. Each describe block contains several it blocks that represent an individual spec. For example, this test begins with a cy.mount, which “is responsible for rendering components within Cypress’s sandboxed iframe and handling any framework-specific cleanup.” This can be placed in a beforeEach with any specific component set-up or properties that are going to be used in each test. This means Cypress will render a HeaderComponent to the DOM and then perform any of the following commands using javascript.
The cy.get(‘header’) is the only following command. We’ll use javascript to get the header element and then assert that it not only exists but is also visible. This test will fail when doing TDD until we add a <header></header> element to the template.
By default, if a user isn’t logged in, the header should say: “Hello, User” in an h1.
it(‘should have an h1 that has a greeting’, () => {
cy.mount(HeaderComponent);
cy.get(‘h1’)
.should(‘have.text’, ‘Hello, User’);
});
Like the previous spec, this one mounts the component before looking for an h1 and then checking that text within the h1 is correct. This confirms our accessible headers and checks that the Input()’s initialization works without any data provided by a consumer. Since there is a pipe, it affirms that the string value we pass doesn’t blow anything up. We should already have peace of mind and confidence in that pipe since it was unit tested in an earlier test.
If a user is logged in, the header should say: “Hello, {{ User Name }}” in an h1.
it(‘should display a user name if name is provided’, () => {
cy.mount(HeaderComponent, {
componentProperties: {
title: {
firstName: ‘John’,
lastName: ‘Doe’
}
}
}); cy.get(‘h1’)
.should(‘have.text’, ‘Hello, John Doe’);
});
This last test is a little more complex. We didn’t wrap each cy.mount before because we knew we’d have a test to provide a user’s first and last names. When mounting a component, you can do a couple of things. The first argument takes a component class name like “HeaderComponent” or the component’s selector, “lib-header.” The selector argument doesn’t work with standalone components since you would have to declare the component in the list of declarations and can’t declare a standaone component in a module. The second argument acts almost like a ngModule where you can pass imports, declarations, and providers. However, unlike a ngModule, we get access to the componentProperties property, where you can modify the properties of a component before it is initialized. Here, we’re setting the title property to an instance of UserName and then grabbing that same h1 from the previous spec and asserting that, instead of “Hello, User,” we get “Hello, John Doe.”
Easy, right?
https://medium.com/media/a7a9be28f6ee5adc8d6614fd7ec03067/href
Summary
Component testing is interesting! It isn’t some new concept, but it is faster than what we’ve had access to until Cypress’s implementation. As you can see in the top right of that left pane, it takes 147ms to run those three tests, and each test takes under a few minutes to write if your component’s template is already defined. With these tests as a starting point for this header component, if someone changes something that doesn’t agree with the three specs that already exist, we know that the component is “broken,” and we need to address it. Since this is only testing the instance of the HeaderComponent, we can’t call it an integration test. We can’t call this a unit test, as no functionality is being tested. This, my friends, is a component test!
Was this already your understanding of Component Testing in Angular? Are your Jasmine or Jest tests this clean and fast? Let me know how yours contrast with this!
See you in the next chapter, where I’ll review integration testing.
Angular Testing: Component Testing was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.