A few Angular component test examples comparing Cypress and test harness.
Yesterday I have attended DevReach Boston conference. I got a chance to sit and listen to Alisa Duncan presentation "Angular Component Test Harnesses FTW". In her presentation Alisa compared Angular component tests using TestBed vs TestBed + Test harnesses. For comparison, here is the code for testing the navigation component.
Ok. Even with the harness, writing component tests is not going to be easy. Yes, the interactions with the component are abstracted away a little into the harness, but it is still a lot of boilerplate code just to get the component mounted. There must be a better way. Luckily, I just came from ng-conf conference, where I taught Cypress testing workshop, including the recently released Cypress Angular Component Testing. Can we try writing Cypress Ng component tests? The existing example tests for the Navigation component test the following:
it('should show \'HOME\' and \'LOG IN\' when user is not signed in', async () => { (Object.getOwnPropertyDescriptor(signinServiceSpy, 'isLoggedIn')?.getas jasmine.Spy).and.returnValue(false); fixture.detectChanges();
it('should show \'HOME\', \'PROFILE\', and \'LOG OUT\' when user is signed in', async () => { (Object.getOwnPropertyDescriptor(signinServiceSpy, 'isLoggedIn')?.getas jasmine.Spy).and.returnValue(true); fixture.detectChanges();
The Angular project is using v14.1.0 which is automatically picked up by Cypress, so there is nothing to install. The config is pretty much the standard
Let's see if we can mount the component following the Cypress Angular documentation. Right next to the component we can create a spec file. We will import the component itself and use the cy.mount command to put the component onto the page.
src/app/nav/nav.component.cy.ts
1 2 3 4 5 6 7
import { NavComponent } from'./nav.component'
describe('NavComponent', () => { it('should create and show the links', () => { cy.mount(NavComponent) }) })
Click on the new spec file and run the test.
It cannot be this easy, can it?! Let's confirm the buttons / links are visible.
Notice that once the component is mounted, we can use the standard Cypress commands to interact with the page. No special commands, abstractions, or adaptors needed.
Let's see if our component is working. Let's see what happens if we click on the "Log in" button. I added .wait(1000) just to make the GIF clearly show each step.
1 2 3 4 5 6 7 8 9 10
it('should create and show the links', () => { cy.mount(NavComponent) cy.contains('a', 'HOME') cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click() cy.contains('a', 'HOME') cy.contains('a', 'PROFILE').should('be.visible') cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click() cy.contains('a', 'HOME') cy.contains('button', 'LOG IN').should('be.visible') })
Our component is working!
Spying on the Signin Service
Ok, the original tests also confirmed the SigninService worked. Well, they just stubbed it, so not really confirming it. We want to check if the SigninService is receiving the expected calls from the Navigation component when we click on the "Log in" and "Log out" buttons.
Let's update our test. The NavComponent gets an instance of the service via its props. The test can create an instance and pass it. The cy.mount command has options for that.
We can see the confirmed calls to the service in the Command Log left column.
Ughh, this is seriously nice. Let's compare it to the original TestBed test variants.
Email subscription component tests
Maybe the navigation component is a weird case especially suitable for Cypress component testing. Let's look at the EmailSubscriptionComponent. The test harness mounts the component and lists a series of imports.
But we still need to pass a function stub as (emailSubscription)="onEmailSubscription($event)" for the component to call when the test clicks the "Add" button.
We use cy.stub to create and pass a Sinon.js stub function as a prop to the component. After clicking on the Add button we confirm that stub was called with expected arguments.
Let's compare this test to the original TestBed vs TestBed + test harness specs.
I know which one I like.
Add comment component test
Let's confirm the AddCommentComponent test works. First, the happy path test
it('should throw when an invalid comment is added', async () => { const el = await loader.getHarness(AddCommentHarness); awaitexpectAsync(el.setComment(' ')).toBeRejectedWithError('Comment is invalid'); });
Where and how does it get "Comment is invalid" error? If we do a text search in the project, we find the custom component harness file
So it is NOT the production component code checking if the comment is all blank spaces. It is our test code... Hmm. This is my argument against using PageObject in general. By adding logic here, we suddenly created a problem and unexpected behavior that is NOT part of the application. Keep it simple. If you use PageObjects, use them for simple consistent access to the page, and not for places to hide the bugs.
it('submits a comment', () => { cy.get('input[type=text]').should('have.value', '') cy.contains('button', 'Add').should('be.disabled') const comment = 'TEST TEST' cy.get('input[type=text]').type(comment) cy.contains('button', 'Add') // cy.click only works if the button is no longer disabled .click() cy.get('@onCommented').should('be.calledOnceWithExactly', comment) })