A Complete Guide to Angular Component Testing with Cypress

calendar_todayschedule2 min read
— Originally published at dev.to

Angular's latest versions have introduced exciting features like signals, computed, and enhanced reactivity. Paired with Cypress Component Testing, developers now have a powerful toolkit to write fast, realistic, and robust tests. This post dives deep into Angular component testing using Cypress, including how to test components that use signals, observables, and mock services with confidence.

✨ Why Cypress for Angular Component Testing?

Cypress isn't just for end-to-end tests. With Cypress Component Testing, you can:

  • Render Angular components in isolation
  • Interact with them as a user would
  • Spy on inputs/outputs
  • Work seamlessly with Angular DI, modules, and modern features like signals

Setting Up Cypress Component Testing
Install Cypress and initialize component testing:

npm install cypress @cypress/angular --save-dev
npx cypress open --component

Follow the UI wizard to configure your Angular workspace. It will auto-generate a Cypress configuration compatible with Angular.

️ Example Component with Signals & Outputs

Let's test a simple CounterComponent that demonstrates:

  • Input binding
  • Output emission
  • Signals and computed values
// counter.component.ts
import { Component, Input, Output, EventEmitter, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <h2>{{ title }}</h2>
      <p>Count: {{ count() }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="reset()">Reset</button>
      <p>Double: {{ doubleCount() }}</p>
    </div>
  `
})
export class CounterComponent {
  @Input() title = 'Default Counter';
  @Output() onReset = new EventEmitter<void>();

  private _count = signal(0);
  count = this._count.asReadonly();
  doubleCount = computed(() => this._count() * 2);

  increment() {
    this._count.set(this._count() + 1);
  }

  reset() {
    this._count.set(0);
    this.onReset.emit();
  }
}

Basic Cypress Tests

✅ Rendering with Default and Custom Inputs

// counter.component.cy.ts
import { mount } from 'cypress/angular';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  it('renders with default title', () => {
    mount(CounterComponent);
    cy.contains('Default Counter');
    cy.contains('Count: 0');
  });

  it('accepts custom title', () => {
    mount(CounterComponent, {
      componentProperties: { title: 'Custom Counter' }
    });
    cy.contains('Custom Counter');
  });
});

⚖️ Testing Signals and Computed Values

it('increments count and updates double', () => {
  mount(CounterComponent);
  cy.get('button').contains('Increment').click().click();
  cy.contains('Count: 2');
  cy.contains('Double: 4');
});

Spying on Output Events

it('emits reset event', () => {
  const resetSpy = cy.spy().as('resetSpy');

  mount(CounterComponent, {
    componentProperties: {
      onReset: { emit: resetSpy } as any
    }
  });

  cy.get('button').contains('Reset').click();
  cy.get('@resetSpy').should('have.been.calledOnce');
});

Testing with Observables

// message.component.ts
@Component({
  selector: 'app-message',
  template: `<p *ngIf="message">{{ message }}</p>`
})
export class MessageComponent {
  private _message = signal('');
  message = this._message.asReadonly();

  @Input()
  set messageInput$(value: Observable<string>) {
    value.subscribe(msg => this._message.set(msg));
  }
}
it('renders observable message', () => {
  const message$ = of('Hello from Observable!');
  mount(MessageComponent, {
    componentProperties: { messageInput$: message$ }
  });
  cy.contains('Hello from Observable!');
});

Advanced Mocking Techniques

⚙️ Injecting Angular Services

class MockLoggerService {
  log = cy.stub().as('logStub');
}

mount(MyComponent, {
  providers: [
    { provide: LoggerService, useClass: MockLoggerService }
  ]
});

Mocking HTTP Calls with HttpClientTestingModule

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

beforeEach(() => {
  cy.intercept('GET', '/api/data', { fixture: 'data.json' });
});

mount(MyHttpComponent, {
  imports: [HttpClientTestingModule]
});

Stubbing Child Components

@Component({
  selector: 'app-child',
  template: '' // stubbed template
})
class StubChildComponent {}

mount(ParentComponent, {
  declarations: [StubChildComponent]
});

Conclusion

Cypress Component Testing is a game-changer for Angular developers. By combining the real browser testing experience with Angular’s new reactivity features like signals and computed, we can create robust, testable, and modern UIs.

Key Takeaways:

  • Mount Angular components with Cypress easily
  • Test signals, computed, observables
  • Use spies and stubs for robust unit isolation
  • Mock services and HTTP with Angular-friendly tooling
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

The Sovereign Vault — A Comprehensive Guide to Protocol-Driven AI

Ken W. Algerverified - Jun 4

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

Your Tech Stack Isn’t Your Ceiling. Your Story Is

Karol Modelski - Apr 9

Tuesday Coding Tip 02 - Template with type-specific API

Jakub Neruda - Mar 10
chevron_left
176 Points5 Badges
3Posts
0Comments
Raju Dandigam is an accomplished Engineering Manager and Staff Engineer with over 15 years of experi... Show more

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!