angulardependency-injectioncypressangular-standalone-componentscypress-component-testing

How to mock a service inside a Cypress Component Test for a Angular Standalone Component


How to provide a mock for a Standalone component in a cypress component test?

I have this mount-function inside my test:

function mount() {
  cy.mount(TestComponent, {
    providers: [
      provideHttpClient(),
      provideHttpClientTesting(),
      { provide: TestService, useValue: mockTestService },
    ],
  });
}

The TestComponent looks like this:

@Component({
  selector: 'app-test',
  standalone: true,
  imports: [TestModule],
  templateUrl: './test.component.html',
  styleUrl: './test.component.scss',
})
export class TestComponent {
  testService = inject(TestService);
}

The TestModule provides the TestService and some other services it depends on (which I don't need for my test, since I want to use a mock).

While the module looks like this:

@NgModule({
  providers: [TestService, OtherService],
})
export class TestModule {}

The problem is, that it doesn't work. The test still calls the original service instead of the mock.


Solution

  • The basic problem is that TestService is provided via TestModule.

    Ideally there should be a way to mock TestModule, but I couldn't find it. However I did find a way to remove TestModule so that your mockTestService if effective.

    Essentially, use the TestBed.overrideComponent() method to do so.

    This is the complete test. Normally TestService.getValue() returns 42, but the mock service returns 33.

    import { provideHttpClient } from '@angular/common/http';
    import { provideHttpClientTesting } from '@angular/common/http/testing'
    import {TestBed} from '@angular/core/testing'
    import {TestComponent} from './test.component'
    import {TestService} from './test.service'
    import {TestModule} from './test.module'
    
    it('mocks TestService.getValue', async () => {
    
      class MockTestService {
        getValue() { return 33 }          // mocked value, should show in template
      }
      const mockTestService = new MockTestService;
    
      await TestBed.configureTestingModule({ imports: [TestComponent]})
        .overrideComponent(TestComponent, {
          remove: {
            imports: [TestModule],         // remove TestModule from component 
          },
        })
        .compileComponents();
    
      cy.mount(TestComponent, {
        providers: [
          provideHttpClient(),
          provideHttpClientTesting(),
          { provide: TestService, useValue: mockTestService },
        ],
      })
    
      cy.get('h1')
        .should('have.text', 'I am TestComponent: 33')    // check mock is used
    })
    

    enter image description here

    With the mock removed, the failing test log:

    enter image description here

    Notes

    1. I had to infer the component code from the information in the question - I haven't used Angular since version 5 and the docs have gaps that I had to guess at, so the component I used may not be exactly the same as yours.

    2. It does not feel quite right to remove the TestModule, I would prefer to mock it. That would probably entail creating MockTestModule which uses MockTestService and import it into cy.mount(). Unfortunately I havn't the time to check that out.


    The sample component

    test.service.ts

    import { Injectable } from '@angular/core';
    
    @Injectable() 
    export class TestService {
      constructor() {}
      getValue() {
        return 42;         // "real" value shown in template if no mock is used
      }
    }
    

    test.component.ts

    import {Component} from '@angular/core';
    import {TestModule} from './test.module'
    import {TestService} from './test.service'
    
    @Component({
      selector: 'app-test',
      standalone: true,
      imports: [TestModule],
      template: `<h1>I am TestComponent: <span>{{value}}</span></h1>`
    })
    export class TestComponent { 
      value = 0;
      constructor(
        private testService: TestService
      ) {
        this.value = testService.getValue()
      }
    }
    

    test.module.ts

    import { provideHttpClient } from '@angular/common/http';
    import {NgModule} from '@angular/core';
    import {TestService} from './test.service'
    
    @NgModule({
      providers: [
        provideHttpClient(),
        {provide: TestService} 
      ],
    })
    export class TestModule {}