Angular jasmine how to spy an async spy method - jasmine

I am new to writing test with async/await in angular.
I have the following code. The service method is an async method. The test fails saying component.options.length is 0.
Can anyone please help me how to fix the error so the options has got the value i set in spy?
Thanks
spec.ts
spySideNavService = jasmine.createSpyObj('SideNavService', [], {
setOrgUserDetails: () => {},
loadMenus: () =>
[
{
id: 'my-menu',
label: 'My Menu',
icon: 'far fa-envelope fa-2x',
url: 'url'
}
] as NavOption[]
});
describe('ngOnInit', () => {
it('should add navigation options', () => {
expect(component.options.length).toBeGreaterThan(0);
});
});
component:
ngOnInit(): void {
this.options = await this.sideNavService.loadMenus();
}
SideNavService:
async loadMenus(): Promise<NavOption[]> {
//logic
}
Tried answer given below but still not working:
describe('ngOnInit', () => {
it('should add navigation options', fakeAsync(() => {
// !! call tick(); to tell the test to resolve all promises
// before coming to my expect line
tick();
expect(component.options.length).toBeGreaterThan(0);
}));
});

You need to use fakeAsync/tick to control promises.
// !! add fakeAsync
it('should add navigation options', fakeAsync(() => {
// !! call tick(); to tell the test to resolve all promises
// before coming to my expect line
// !! call ngOnInit
component.ngOnInit();
console.log(component.options);
tick();
console.log(component.options);
expect(component.options.length).toBeGreaterThan(0);
}));
Before, the test would go to the await line and go back to the test for the expect because the await is saying to do this later. Now with the tick, we are saying if they are any promises created, resolve them before moving forward.
Also, I think you're missing a Promise.resolve on loadMenus.
loadMenus: () => Promise.resolve(
[
{
id: 'my-menu',
label: 'My Menu',
icon: 'far fa-envelope fa-2x',
url: 'url'
}
] as NavOption[])
I am thinking the Promise.resolve is required so it can be awaited.
edit
I don't think the done callback will help you.
You can try using await fixture.whenStable() to wait for the promise(s). Try this:
describe('ngOnInit', () => {
it('should add navigation options', async () => {
component.ngOnInit();
await fixture.whenStable();
expect(component.options.length).toBeGreaterThan(0);
});
});

Related

Cypress: Why finicky in handling spec's after hook?

Blocked by a Cypress custom command not executing properly within a spec's after hook when a test failed. Experimented with the following minimal spec examples.
Context
Testing two Cypress custom commands:
cy.cmdLogin(): Standard username password authentication
cy.cmdLogout(): cy.get('logout_button').click()
Spec #1, cy.cmdLogout() in after hook does work
Very simple Cypress tests spec, test cy.cmdLogin() with after hook with cy.cmdLogout(), works perfectly:
context('SPEC Login', () => {
after('AFTER Logout', () => {
if (this.successLogin) {
cy.cmdLogout().then(() => {
cy.log('Logout Success');
});
}
});
it('TEST Login', () => {
cy.cmdLogin().then(() => {
cy.log('Login Success');
this.successLogin = true;
});
});
});
Spec #2, cy.cmdLogout() in after hook does not work
Modified Cypress tests spec, test cy.cmdLogin() with after hook with cy.cmdLogout(), now added a test that forced failure expect(false).to.be.a('boolean').to.be.true;. The after hook cy.cmdLogout() is called and fails:
Verified that Cypress could handle cy.get('logout_button') without failure.
Yet, extending with .click(), cy.get('logout_button').click(), Cypress would throw an error: Can not perform click() on undefined.
context('SPEC Login', () => {
after('AFTER Logout', () => {
if (this.successLogin) {
cy.cmdLogout().then(() => {
cy.log('Logout Success');
});
}
});
it('TEST Login', () => {
cy.cmdLogin().then(() => {
cy.log('Login Success');
this.successLogin = true;
});
});
it('TEST Fail', () => {
expect(false).to.be.a('boolean').to.be.true;
});
});
Spec #3, removed after hook, cy.cmdLogout() moved to another test does work
Modified Cypress tests spec again and removed after hook, test cy.cmdLogin(), test forced failure, and now test cy.cmdLogout(). This works perfectly:
context('SPEC Login', () => {
it('TEST Login', () => {
cy.cmdLogin().then(() => {
cy.log('Login Success');
this.successLogin = true;
});
});
it('TEST Fail', () => {
expect(false).to.be.a('boolean').to.be.true;
});
it('TEST Logout', () => {
if (this.successLogin) {
cy.cmdLogout().then(()=> {
cy.task('log', 'Logout Success');
});
}
});
});
Insight as to why Cypress is finicky with spec's after hook would be very much appreciated
The short answer (for the general situation) is change after() hook to before() hook, but in your case that wouldn't work because this.successLogin will always be undefined inside a before.
You would need to create a cy.isLoggedIn() command or call cy.cmdLogout() unconditionally.
To get after() the logic working on a fail, use
after('AFTER Logout', () => {
cy.cmdLogout()
});
Cypress.on('fail', (error, runnable) => {
cy.cmdLogout()
throw error
})
which is equivalent to this in plain javascript
try {
some-code-that-might-error()
cy.cmdLogout() // does not run if error occurs
} catch (error) {
cy.cmdLogout() // do the logout in error situation
}

having multiple describe in spec leads to weird behaviour

I have 2 describe blocks in a spec file.
First, describe visits xyz.com and Second, describe visits abc.com
And I need these 2 describe in one spec only. The wired behavior I see is it runs the tests smoothly but after visiting abc.com from 2nd describe it starts running 1st describe again. An infinite loop of tests
var signedOutArtifactID = null;
describe('WEB APP E2E tests', function() {
var token = null;
before(function() {
cy.visit('/');
// Login
cy.get('#username')
.type(auth.geneticist.username);
cy.get('#password')
.type(auth.geneticist.password);
cy.get('button')
.contains('Login')
.click()
.should(function() {
token = localStorage.getItem('token');
expect(token).not.to.be.null;
});
});
beforeEach(function() {
localStorage.setItem('token', token);
cy.contains('Logout')
.should('exist');
expect(localStorage.getItem('token'));
});
it('should land on home page', function() {
cy.url()
.should('include', '/home');
});
it('should save and generate and end up on signout page', function() {
cy.contains('Save and Generate Report')
.click();
cy.url()
.should('include', '/sign-out');
});
it('should signout and send successfully', function() {
cy.url()
.should(function(currentURL) {
signedOutArtifactID = currentURL.match(/2-([0-9]+)/)[0];
expect(signedOutArtifactID).not.to.be.null;
});
// Make sure interpretation was updated
cy.get('.card-body pre')
.should('contain', 'test interpretation added by cypress');
cy.contains('Sign Out and Send')
.click();
cy.contains('Yes, sign out and send')
.click();
});
});
describe('2nd WEB APP E2E tests', function() {
before(function () {
cy.visit({
url:`https://webappurl.com/search?scope=All&query=${signedOutArtifactID}`,
failOnStatusCode: false
})
})
it('Review Completed step in clarity', async () => {
cy.get('#username').type(auth.clarity_creds.username)
cy.get('#password').type(auth.clarity_creds.password)
cy.get('#sign-in').click()
cy.get('.result-name').click()
cy.get('.view-work-link').contains('QWERTYU-IDS').click()
cy.get('.download-file-link ')
.should(($downloads) => {
expect($downloads).to.have.length(2)
})
});
});
describe defines a test suite. You can only have one top-level test suite per file, and only one domain per test.
I would just change your describes to contexts and wrap both contexts in a single describe, like so:
var signedOutArtifactID = null;
describe('e2e tests', function() {
context('WEB APP E2E tests', function() {
var token = null;
before(function() {
cy.visit('/');
// Login
cy.get('#username')
.type(auth.geneticist.username);
cy.get('#password')
.type(auth.geneticist.password);
cy.get('button')
.contains('Login')
.click()
.should(function() {
token = localStorage.getItem('token');
expect(token).not.to.be.null;
});
});
beforeEach(function() {
localStorage.setItem('token', token);
cy.contains('Logout')
.should('exist');
expect(localStorage.getItem('token'));
});
it('should land on home page', function() {
cy.url()
.should('include', '/home');
});
it('should save and generate and end up on signout page', function() {
cy.contains('Save and Generate Report')
.click();
cy.url()
.should('include', '/sign-out');
});
it('should signout and send successfully', function() {
cy.url()
.should(function(currentURL) {
signedOutArtifactID = currentURL.match(/2-([0-9]+)/)[0];
expect(signedOutArtifactID).not.to.be.null;
});
// Make sure interpretation was updated
cy.get('.card-body pre')
.should('contain', 'test interpretation added by cypress');
cy.contains('Sign Out and Send')
.click();
cy.contains('Yes, sign out and send')
.click();
});
});
context('2nd WEB APP E2E tests', function() {
before(function () {
cy.visit({
url:`https://webappurl.com/search?scope=All&query=${signedOutArtifactID}`,
failOnStatusCode: false
})
})
it('Review Completed step in clarity', async () => {
cy.get('#username').type(auth.clarity_creds.username)
cy.get('#password').type(auth.clarity_creds.password)
cy.get('#sign-in').click()
cy.get('.result-name').click()
cy.get('.view-work-link').contains('QWERTYU-IDS').click()
cy.get('.download-file-link ')
.should(($downloads) => {
expect($downloads).to.have.length(2)
})
});
});
})
There should be one describe block per suite (specification file). Therefore, when I need to wrap multiple related tests in one specification file I use context. Also, the following is what cypress documentation says:
The test interface, borrowed from Mocha, provides describe(),
context(), it() and specify().
context() is identical to describe() and specify() is identical to
it(), so choose whatever terminology works best for you
However, I believe the test structure and describe, context, it hierarchy is a little off track. So, here is how I write tests:
describe('User Authentication Using Custom Auth Token', () => {
beforeEach(() => {
cy.on('uncaught:exception', err => {
console.log('cypress has detected uncaught exception', err);
return false;
});
});
context('when not authenticated', () => {
it('Redirects to /login and Stays on /Login', () => {
cy.visit('/');
cy.location('pathname').should('equal', '/login');
// more on your logic
});
context('when authenticated', () => {
it('Successful login using Custom Auth Token', ()=>{
cy.visit('/')
cy.login();
// more on your logic
});
});
});

Testing ngrx Effects with Jasmine spy

I am writing an ngrx effect and trying to test it. However, the effect calls a service that calls an API that will require authentication. As a result, I am trying to create a spy in Jasmine to handle returning the data. This is my first time using ngrx effects, so I am really unsure where to put different parts of the code. Nothing I have done is allowing this test to run correctly.
The effect is a very simple one as follows:
#Effect() itemSelected: Observable<Action> = this.d.pessimisticUpdate('ITEM_SELECTED', {
run: (action: ItemSelected) => {
return this.myService.getItemById(action.payload).map((res) => ({
type: 'ITEM_INFO_RETURNED',
payload: res
}));
},
onError: (a: ItemSelected, error) => {
console.error('Error', error);
}
});
constructor(private d: DataPersistence<ItemState>, private myService: MyService) {
// add auth headers here
}
My test is currently written as follows:
describe('ItemEffects', () => {
let actions: Observable<any>;
let effects: ItemEffects;
let myService = jasmine.createSpyObj('MyService', ['getItemById']);
let item1: Item = {id: 1, name: 'Item 1'};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({}),
],
providers: [
ItemEffects,
DataPersistence,
provideMockActions(() => actions),
{
provide: MyService,
useValue: myService
}
],
});
effects = TestBed.get(ItemEffects);
});
describe('someEffect', () => {
it('should work', async () => {
myService.getItemById.and.callFake(function (id) {
return items.find((r) => r.id === id);
});
actions = hot('-a-|', { a:{ type:'ITEM_INFO_RETURNED', payload:1}});
expect(effects.itemSelected).toEqual(
{ type: 'ITEM_INFO_RETURNED', payload: { item1 } }
);
});
});
});
This is still attempting to use the production MyService (requiring authentication). If I move the myService override out of the provider and into the actual test,
TestBed.overrideProvider(MyService, { useValue: myService });
I get an error that it cannot read the property "itemSelected" of undefined, which would be when I am calling the effects at the very end of the test.
I am really new to ngrx, as well as to TestBed. Is there somewhere else I should be defining this Jasmine spy? Should I be using something other than createSpyOn for this?
Thanks in advance!

Why won't VueJS invoke methods from the created() function?

Learning VueJS and trying to do a simple API call on component load to put a list of repos onto my page. When I call and set the this.repos from the created() method, no problem. But if I set it as a method and then call it from this.getRepos nothing happens. No error, nothing. What am I missing about VueJS?
This works:
data: () => ({
msg: 'Github Repos',
ok: 'Im practically giving away these repos',
repos: [],
}),
methods: {
},
async created() {
const repos = await axios.get('https://api.github.com/orgs/octokit/repos');
this.repos = repos.data.map(repo =>
`<div class="box"><a href="${repo.html_url}">
${repo.name}
</div>`,
);
},
This DOES NOT work:
data: () => ({
msg: 'Github Repos',
ok: 'Im practically giving away these repos',
repos: [],
}),
methods: {
getRepos: async () => {
const repos = await axios.get('https://api.github.com/orgs/octokit/repos');
this.repos = repos.data.map(repo =>
`<div class="box"><a href="${repo.html_url}">
${repo.name}
</div>`,
);
},
},
created() {
this.getRepos();
},
Any ideas? Thanks!
It's simply because you used arrow functions here so that this.repos's this is bound to window object. Changing async () => {} to async function() {} will help you overcome it.
See demo
Note that you should not use an arrow function to define a method (e.g. plus: () => this.a++). The reason is arrow functions bind the parent context, so this will not be the Vue instance as you expect and this.a will be undefined.
reference
Another way to do an Axios call with Vue using then() method:
demo
created() {
axios.get('https://api.github.com/orgs/octokit/repos', {
params: {
type: 'all',
},
})
.then((res) => {
console.log('Success Response', res.data);
res.data.forEach((repo) => {
this.repos.push({ name: repo.name, url: repo.html_url, language: repo.language });
});
})
.catch((err) => {
console.log('Error', err);
});
},

mocha tests Is it possible to reuse the same before() and after() hooks in all my tests?

I use the same before() and after() root level hooks in all my tests to setup and clear my test-database ...
Is there any way to move them into a file and export/import that file ?
Yes. I was able to achieve this behavior by taking advantage of mocha's this context, discussed in their Wiki article about shared behaviors. I am using ts-mocha which accounts for the async/await functionality.
So I wrote functions to login and logout using supertest-session and it looks something like this:
export function authHooks()
{
const email = UsersSeed[0].email;
const password = UsersSeed[0].password;
before(`login ${email}`, async function()
{
await this.session.post('/api/account/login')
.send({ email, password })
.expect(201);
});
after(`logout ${email}`, async function()
{
await this.session.get('/api/account/logout')
.expect(200);
});
}
Inside of my describe I set up the this.session and in my it-statements I can reuse it. It looks a little like this:
import { expect } from 'chai';
import * as Session from 'supertest-session';
import { authHooks } from './authHooks';
describe('Some Suite', () =>
{
before(function()
{
this.session = new Session(SomeApp);
});
after(function()
{
this.session.destroy();
});
describe('when not authenticated', () =>
{
it('returns 403', async function()
{
await this.session.get('/api/jobs')
.expect(403)
.expect({ statusCode: 403, message: 'Forbidden resource' });
});
});
describe('when authenticated', () =>
{
authHooks();
describe('when finding jobs', () =>
{
it('returns all jobs', async function()
{
await this.session.get('/api/jobs')
.expect(200)
.then((response) =>
{
expect(response.body).to.deep.eq(SomeThing);
});
});
});
});
});
I'm not sure if this is the best way to achieve it (I'm not a fan of using function over () => {} personally), but I have confirmed it works.
The above code will not run mainly because I'm protecting code specifics, but hopefully this will provide at least one option for you and maybe someone else can show a better way to do this.

Resources