6

I have an Angular Application with Jasmine Testing Framework. The Application has a Service called AuthService that handles decoding JSON Web Tokens:

auth.service.ts

import * as jwtDecode from 'jwt-decode';
...

@Injectable()
export class AuthService {
  ...
  public getTokenPayload(token) {
    return jwtDecode(token);
  }
}

Now I would like to stub the module jwtDecode and return a fake value for the purpose if testing:

auth.service.spec.ts

...

it('should get a token payload', () => {
  const fakeToken = 'fake-token';

  spyOn(service, 'getTokenPayload').and.callThrough();
  const tokenPayload = service.getTokenPayload(fakeToken);

  expect(tokenPayload).toBe('fake-token');
});

Because 'fake-token' is not a valid JSON Web Token, my tests fail with the message:

InvalidTokenError: Invalid token specified: undefined is not an object (evaluating 'str.replace')

This is probably an error generated from the jwt-decode module, which is expected. I do not want to have to include another module just to create valid JSON Web Tokens for testing purposes. Instead, I'd like to stub the functionality of jwtDecode().

What I've tried

1. Using spyOn on jwtDecode

When I use spyOn, I need an object with a method. So for the imported jwtDecode this won't work, since it is a function itself:

spyOn(jwtDecode, '<method?>').and.callFake(() => 'fake-token');

2. Using callFake on getTokenPayload

I've tried using:

spyOn(service, 'getTokenPayload').and.callFake(() => 'fake-token');

...and that prevents any errors from happening. However, my code coverage report now shows that the function getTokenPayload is not covered. Moreover, I have other function in the application that use external NPM Modules and I don't want to ignore code coverage since they might have other implementations inside the method that should be tested.

3. Using createSpy on jwtDecode

I tried overriding the imported jwtDecode and create a spy:

const jwtDecode = jasmine.createSpy('jwtDecode').and.returnValue('fake-token');

This gives me the same error as above, indicating that jwtDecode is not overridden inside my actual AuthService Service.

4. Using the window as the Object

From this question I read that global modules might be attached to the window Object. Hence, I tried doing the same thing for jwt-decode:

inside the test...

console.log(window.jwtDecode); // undefined
console.log(window.jwt_decode); // undefined

Unfortunately, both values are undefined on the window Object.

Question

I guess in general the question becomes:

How to stub imported NPM modules? Especially, how to stub them if they are not an object, but a function (without a method to use in Jasmine spyOn)?

Nicky
  • 3,607
  • 6
  • 33
  • 64
  • I think you can spy on window.jwtDecode. Take a look at https://stackoverflow.com/questions/9510148/using-jasmine-to-spy-on-a-function-without-an-object – João Mendes Jan 27 '18 at 17:51
  • Thank you for the answer. Unfortunately I came across this method earlier and tried without luck. I've updated the question, providing my findings in this method. – Nicky Jan 27 '18 at 18:19

2 Answers2

9

You're very close! Since a service is just a class, the best way to test it is to instantiate a new one and spy on it, as it appears you are doing. If you want to spy on the imported method, you will need to somehow include it in your service. Otherwise, there is no way for your test to know what that method is.

So have a property on your service:

jwtDecode = jwtDecode; // the imported one

And called it as this.jwtDecode in your getTokenPayload method.

Then the following will work:

const service = new AuthService( /* constructor args */ );

const jwtDecode = spyOn(service, 'jwtDecode');
jwtDecode.and.returnValue('fake-token');
vince
  • 7,808
  • 3
  • 34
  • 41
  • Thank you for replying! I've tried this solution, but I thik the syntax is not correct (it's not working either). The second parameter of `createSpy` should be a function (as per the docs). Also, does the first parameter `'service'` refer to anything like a variable name? I did try `const mockService = jasmine.createSpy('service', jwtDecode).and.returnValue('fake-token');` and `const mockService = jasmine.createSpy('AuthService', jwtDecode).and.returnValue('fake-token');` but without any luck. Any ideas? – Nicky Jan 27 '18 at 18:32
  • Oops - I meant `.createSpyObj` -- updating my answer now – vince Jan 27 '18 at 18:33
  • And I'm not sure... I'm reviewing your question now and thinking. – vince Jan 27 '18 at 18:36
  • The `createSpyObj` syntax is now correct, but the `mockService` value is just `Object{jwtDecode: function () { ... }}`, so I can't test `getTokenPayload` on that Object. I've also tried with `'service'` and `'AuthService'` as first parameters to `createSpyObj` but without luck. The `mockService` remains `Object{jwtDecode: function () { ... }}`. – Nicky Jan 27 '18 at 18:46
  • Hmm.. My mistake. My answer was for if you had used the `TestBed` to set up a component and added your service as a provider. I just updated the answer for testing a service. Let me know if that doesn't work, I may need to see more of your code if it does not. – vince Jan 27 '18 at 19:02
  • That doesn't work either. When I use `spyOn(service, 'jwtDecode')` it implies that `jwtDecode` should be a method on the `AuthService` class, which it is not (which is why I'm having such a hard time with this :(). The `jwtDecode` is only imported at the top using `import * as jwtDecode from 'jwt-decode';` and then used **inside** a class method called `getTokenPayload`. I still have no idea about how to stub an imported module like that. I could, of course, add `jwtDecode` as a class property, with `public jwtDecode = jwtDecode`, but that doesn't seem like a good idea right? – Nicky Jan 27 '18 at 19:08
  • Ohhhhh... now I understand what you mean by stubbing an imported module. If it's not a method on your service, should you be testing it in your service's spec? – vince Jan 27 '18 at 19:13
  • I'm sorry for the confusion! You're question is a good one, but I should be able to simply stub a module right? I'm looking for consistent code coverage on class methods using external modules like this one, because my auth process heavily depends on some of these methods using external code. So I thought I'd stub them such that my test becomes independant of these external modules. Is that a wrong approach maybe? – Nicky Jan 27 '18 at 19:16
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/164011/discussion-between-vincecampanale-and-nicky). – vince Jan 27 '18 at 19:16
-1

Simply spy on the default export from the jwt-decode module, which is the jwtDecode function. This saves having to pollute your service as suggested.

As an example, in your spec file:

import * as jwt from 'jwt-decode';
...
// return whatever data structure you require
spyOn(jwt, 'default').and.returnValue({ someData: ['x', 'y', 'z'] });
Kevin
  • 1