Overriding providers on a test-by-test basis in Angular

Viewed 316

I would like to override a service provider when testing Ngrx Effects on a per-test basis to cover a success and failure response.

What I've tried so far is providing my service in the TestBed as such:

describe('Account Effects', () => {

  let actions: ReplaySubject<any>;
  let effects: AccountEffects;
  let store: Store<IAccountsState>;

  const email = 'someone@gmail.com';
  const password = 'abc123!';

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule,
      ],
      providers: [
        AccountEffects,
        provideMockActions(() => actions),
        provideMockStore<IAccountsState>({ initialState }),
        {
          provide: AccountsService,
          useValue: { _signIn: () => of(null) }
        }
      ]
    });

   store = TestBed.get(Store);
  });


  beforeEach(async () => {
    effects = TestBed.get(AccountEffects);
  });
});

Then in each test, override the provider:

describe('signIn$ success', () => {
  beforeEach(() => {
    TestBed.overrideProvider(AccountsService, {
      useValue: {
        _signIn: () => of({ authenticated: true })
      }
    });
  });
    
  it('should dispatch signInSuccess when a successful response is received', () => {
    actions = new ReplaySubject(1);
    actions.next(AccountActions.signIn({ email, password }));
    
    effects.signIn$.subscribe(result => {
      expect(result).toEqual(AccountActions.signInSuccess({ email }));
    });
  });
});

The problem I'm having is that in the test, accountService._signIn is not a function:

An error was thrown in afterAll TypeError:

this.accountsService._signIn is not a function

I need to be able to override the value returned in each test case.

I have been following:

https://indepth.dev/testing-and-faking-angular-dependencies/

What am I doing wrong?

2 Answers

I think you should change the mocking of AccountsService .Account serrvice is a class and you are mocking with a function. You should change the spec as below

class  AccountsServiceMock {
    _signIn(){
       of(null)
    }
}

TestBed.configureTestingModule({
  imports: [
    RouterTestingModule,
  ],
  providers: [
    AccountEffects,
    provideMockActions(() => actions),
    provideMockStore<IAccountsState>({ initialState }),
    {
      provide: AccountsService,
      useClass: AccountsServiceMock 
    }
  ]
});

If you want to change the return value of signin in each spec , you can spyOn it and set the return the value.Hopes this will do the job.

So I can't be totally sure but I would guess you're having trouble because of when you get your effects instance (though I'm not super familiar with Ngrx). Because you're using ioc the effects instance gets created before you assign a value to actions and before you change the definition of AccountsService so your test sort of looks like this:

it('should dispatch signInSuccess when a successful response is received', () => {
  let accountsService: AccountsService = {
    _signIn: () => of({ authenticated: true })
  };
  let actions;
  let effects = new AccountEffects(actions, accountsService);
  accountsService = {
    _signIn: () => of({ authenticated: true })
  };

  actions = new ReplaySubject(1);
  actions.next(AccountActions.signIn({ email, password }));

  effects.signIn$.subscribe(result => {
    expect(result).toEqual(AccountActions.signInSuccess({ email }));
  });
});

Now I'm not totally sure why it's not seeing that first _signIn function. I'd try logging the value of accountsService to console in your effects class to see what's going on, but either way you're going to run into some issues based on the order in which you're defining your values.

So in order to fix your test I would start by assigning effects inside your test to see if that works. Then you can adjust things as needed to make sure you can override the values when you want.

So something like this:

  it('should dispatch signInSuccess when a successful response is received', () => {
    actions = new ReplaySubject(1);
    actions.next(AccountActions.signIn({ email, password }));
    effects = TestBed.get(AccountEffects);

    effects.signIn$.subscribe(result => {
      expect(result).toEqual(AccountActions.signInSuccess({ email }));
    });
  });
Related