How would I approach writing a unit test for this function? When the form is submitted, it sends a POST request to get a login token. With the token response it then sends another request to get the specific user details.
Here is the function:
// Log the user in and save the token, then get the details
onLoginSubmit() {
this.loggingIn = true;
// request the token then the details for the user based off the token
this.userService
.LoginUser(this.loginForm.value.username, this.loginForm.value.password)
.pipe(
tap(res => {
// console.log('Obtained Token:', res);
localStorage.setItem('login_token', res.token);
// this.utilityService.SetLocalStorage('login_token', res.token, 1, 'd');
}),
concatMap(() => this.userService.GetTokenDetails()),
)
.subscribe(
res => {
// console.log('Obtained User Data:', res);
localStorage.setItem('user', res.user);
// this.utilityService.SetLocalStorage('user', res.user, 1, 'd');
this.NavigateToRoute();
},
() => {
this.loggingIn = false;
},
);
}
Here are the service functions:
// logs in the user
LoginUser(email, password): Observable<any> {
return this.utilityservice.post('universal/login', { email, password }, this.utilityservice.GetHeaders());
}
// gets details of the current logged in user
GetTokenDetails() {
return this.utilityservice.get('universal/userAPI', { whoami: '' });
}
For unit tests I am assuming I need to create a mock service where these functions will return a valid response like the following:
// logs in the user
LoginUser(email, password): Observable<any> {
return { res: { token: 'test' } };
}
Is this the right approach or am I completely wrong? Also when it comes to E2E testing for my login page, I am essentially simulating a user in each test by writing code that simulates clicking form buttons and inputting values then checking to make sure I get valid responses or am I over complicating it?
Most test libraries, like Jasmine or Mocha, will contain mocking tools to help you write these tests; you are otherwise on the right track.
Your unit tests should not test the services, but only the code under test, which in this case appears to be a component.
I personally use the RxJs libraries in my spec's - so I may create a mock class like this, and provide an instance:
class MockUserService {
public loginSubject = new Subject();
public loginCalls = [];
// logs in the user
public LoginUser(email, password): Observable<any> {
this.loginCalls.push({ email, password });
return this.loginSubject.asObservable();
}
}
So that elsewhere in my specs I can do something like this:
it('calls login on the user service', () => {
expect(mockUserService.loginCalls.length > 0).toBe(true);
});
it('sets value on local storage', fakeAsync(() => {
mockUserService.loginSubject.next({ res: { token: 'test-token' }});
flushMicrotasks();
expect(mockLocalStorage.setItem).toHaveBeenCalledWith('login_token', 'test-token', 1, 'd');
}));
It is common to use Jasmine for specs, so you may actually set it up more like this (the example spec's here use jasmine's structure).
loginSubject = new Subject();
const mockUserService = jasmine.createSpyObj('UserService', ['LoginUser']);
mockUserService.LoginUser.and.returnValue(loginSubject.asObservable());
There is a lot more to set up in order for this to work properly; dependency injection is awesome because it enables you to write spec's, but it also adds some boilerplate. I typically configure my tests at the top like so:
describe('LoginComponent', () => {
let loginSubject: Subject<any>;
let mockLocalStorage: jasmine.SpyObj<LocalStorageService>;
let fixture: ComponentInstance<LoginComponent>;
beforeEach(() => {
loginSubject = new Subject();
const mockUserService = jasmine.createSpyObj('UserService', ['LoginUser']);
mockUserService.LoginUser.and.returnValue(loginSubject.asObservable());
mockLocalStorage = jasmine.createSpyObj('LocalStorage', ['setItem']);
TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [
{ provide: UserService, useValue: mockUserService },
{ provide: LocalStorageService, useValue: mockLocalStorage }
]
});
fixture = TestBed.createComponent(LoginComponent);
});
it('calls login on the user service', () => {
expect(mockUserService.loginCalls.length > 0).toBe(true);
});
it('sets value on local storage', fakeAsync(() => {
loginSubject.next({ res: { token: 'test-token' }});
flushMicrotasks();
expect(mockLocalStorage.setItem).toHaveBeenCalledWith('login_token', 'test-token', 1, 'd');
}));
});
I am making a ton of assumptions (like that you have inlined your template somehow, otherwise you need to async your beforeEach when creating your component).
In the end, you should probably check out the manual here: https://angular.io/guide/testing