I'm trying to write a test that will run a GET over all items. To do this, I get that list in the before block, then I want to have an it block for each item. I am trying to do this by putting the it block inside itemList.forEach. However, I suspect that the problem here is that the blocks never get registered for the test. How can I run this test as desired?
let token;
let itemList;
describe('GET items/:itemId with Admin', async () => {
before(async () => {
// NOTE: item.find({}) returns a promise of a list of all items
itemList = await item.find({});
console.log(item[0]._id) // this logs correctly!
const res = await userLogin(admin);
token = res.body.accessToken.toString();
});
it('registers initial it test', () => {
// This test passes and logs the statement
console.log('first test registered')
console.log(itemList.length) // successfully logs non-zero value
})
await itemList.forEach(async (item) => {
it('respond with json with a item', () => {
const itemId = item._id;
return getItem(itemId, token)
.then((response) => {
assert.property(response.body, '_id');
});
});
});
});
Afaik the before setup runs before every it test. It doesn't run immediately, and definitely does not wait for anything until you try to iterate your itemList. I think you will need to do either
describe('GET items/:itemId with Admin', async () => {
let token;
before(async() => {
const res = await userLogin(admin);
token = res.body.accessToken.toString();
});
// a list of all items for which tests should be created
const itemList = await item.find({});
console.log(itemList.length) // successfully logs non-zero value
for (const item of itemList) {
it('responds with json for item '+item, () => {
const itemId = item._id;
return getItem(itemId, token).then((response) => {
assert.property(response.body, '_id');
});
}
});
or
describe('GET items/:itemId with Admin', () => {
let itemList;
let token;
before(async() => {
[itemList, token] = await Promise.all([
item.find({}),
userLogin(admin).then(res => res.body.accessToken.toString())
]);
});
it('responds with json for every item', () => {
return Promise.all(itemList.map(item => {
const itemId = item._id;
return getItem(itemId, token)
.then((response) => {
assert.property(response.body, '_id');
});
});
}));
});
});
This is the solution I ended up with. I ended up putting a new describe block in the before block. The before block results the promise that gives the list of items. There is an it block in the top level so that mocha registers the test in the first place.
describe('GET items/:itemId with Admin', async () => {
before((done) => {
Item.find({}).then(async (itemList) => {
// create the admin user to get the items with
await createUsers([admin]);
const res = await userLogin(admin);
const token = res.body.accessToken.toString();
itemList.forEach((item, index) => {
const itemId = item._id;
describe(`get item number ${index}: _id: ${itemId}`, () => {
it('responds with item id', () =>
getItem(item, token)
.expect(200)
.then((response) => {
assert.notProperty(response.body, 'error');
assert.property(response.body, '_id');
assert.equal(response.body._id, itemId);
}));
});
});
done();
});
});
// If there is no it block here, it will not run the before block!
it(`register the initial it`, () => {
assert.equal('regression test!', 'regression test!');
});
});
Related
I am quite new to Cypress and I have some before() calling commands that create bunch of things via API calls and return the IDs of created which I use in the after() for removing them, but somehow it works perfectly if I only return one ID and store in the alias but will fail if I store an array of IDs in alias, is this intended or I did something wrong.
in my code:
before(() => {
cy.setupEnv()
.as('access_token')
.then((token) => cy.setupFlow(token).as('data_id'))
})
after(function () {
console.log(this.access_token)
console.log(this.data_id)
})
console.log(this.data_id) shows fine if setupFlow returns only one ID but becomes undefined if I try to return [id1,id2,id3]and store the array using .as("data_id")
You've struck a strange issue, worth raising with Cypress.
It only seems to happen if you have more than one test.
For example, if I run the following it logs the array.
before(() => {
cy.wrap(1).as('access_token')
cy.then(() => {
return [1,2,3]
}).as('data_id')
})
after(function () {
console.log(this.access_token) // 1
console.log(this.data_id) // [1,2,3]
})
it('test1', () => {
console.log('test1')
expect(true).to.eq(true)
})
If I add a test it logs undefined!
before(() => {
cy.wrap(1).as('access_token')
cy.then(() => {
return [1,2,3]
}).as('data_id')
})
after(function () {
console.log(this.access_token) // 1
console.log(this.data_id) // undefined
})
it('test1', () => {
console.log('test1')
expect(true).to.eq(true)
})
it('test2', () => {
console.log('test2')
expect(true).to.eq(true)
})
One way around this is to use Cypress.env() instead
before(() => {
cy.wrap(1).as('access_token')
cy.then(() => {
Cypress.env('data_id', [1,2,3])
return [1,2,3]
}).as('data_id')
console.log('before')
})
after(function () {
console.log(this.access_token) // 1
console.log(this.data_id) // undefined
console.log(Cypress.env('data_id')) // [1,2,3]
})
beforeEach(function() {
console.log(cy.state())
console.log(this.data_id)
cy.wrap(this.data_id).as('data_id')
})
it('test1', () => {
expect(true).to.eq(true)
console.log('test1')
})
it('test2', () => {
console.log('test2')
expect(true).to.eq(true)
})
Assuming that cy.setupFlow(token) generates an array of values something like [id1, id2, id3]. This will work even when there is one value in the array. You after each should look this:
after(function () {
cy.get('#data_id').then((data_id) => {
//Get individual values
cy.log(data_id[0])
cy.log(data_id[1])
cy.log(data_id[2])
//Get all values using forEach
data_id.forEach((id) => {
cy.log(id) //get all values one by one
})
})
})
I created a small POC for this and it is working as expected.Below are the results.
Code:
describe('SO Ques', () => {
before(function () {
cy.wrap([1, 2, 3]).as('array')
})
it('SO Ques', function () {
cy.log('Hello')
})
after(function () {
cy.get('#array').then((array) => {
cy.log(array[0])
cy.log(array[1])
cy.log(array[2])
})
})
})
Result:
We are using .pipe(takeUntil) in the logincomponent.ts. What I need is, it should get destroyed after successful log in and the user is on the landing page. However, the below snippet is being called even when the user is trying to do other activity and hitting submit on the landing page should load different page but the result of submit button is being overridden and taken back to the landing page.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).subscribe(
(result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
// check to see if we're authed (but don't keep listening)
this.auth.authed
.pipe(takeUntilComponentDestroyed(this))
.subscribe((payload: IJwtPayload) => {
if (payload) {
this.auth.accountO
.pipe(takeUntilComponentDestroyed(this))
.subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
}
}
}
}
);
ngOnDestroy() {}
Custom Code:
export function takeUntilComponentDestroyed(component: OnDestroy) {
const componentDestroyed = (comp: OnDestroy) => {
const oldNgOnDestroy = comp.ngOnDestroy;
const destroyed$ = new ReplaySubject<void>(1);
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
destroyed$.next(undefined);
destroyed$.complete();
};
return destroyed$;
};
return pipe(
takeUntil(componentDestroyed(component))
);
}
Please let me know what I am doing wrong.
Versions:
rxjs: 6.5.5
Angular:10.0.8
Thanks
I've done a first pass at creating a stream that doesn't nest subscriptions and continues to have the same semantics. The major difference is that I can move takeUntilComponentDestroyed to the end of the stream and lets the unsubscibes filter backup the chain. (It's a bit cleaner and you don't run the same code twice every time through)
It's a matter of taste, but flattening operators are a bit easier to follow for many.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).pipe(
tap((result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
}),
mergeMap((result: any) => this.auth.authed),
filter((payload: IJwtPayload) => payload != null),
mergeMap((payload: IJwtPayload) => this.auth.accountO),
takeUntilComponentDestroyed(this)
).subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
});
This function doesn't create another inner stream (destroyed$). This way is a bit more back to the basics so it should be easier to debug if you're not getting the result you want.
export function takeUntilComponentDestroyed<T>(comp: OnDestroy): MonoTypeOperatorFunction<T> {
return input$ => new Observable(observer => {
const sub = input$.subscribe({
next: val => observer.next(val),
complete: () => observer.complete(),
error: err => observer.error(err)
});
const oldNgOnDestroy = comp.ngOnDestroy;
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
sub.unsubscribe();
observer.complete();
};
return { unsubscribe: () => sub.unsubscribe() };
});
}
In Mocha test beforeEach hook, I am trying to destroy all table records.
import { db } from '../src/db/models';
export const truncateTable = () => {
const promises = Object.keys(db).map(key => {
if (key !== 'Sequelize' && key !== 'sequelize') {
console.log(key);
return db[key].destroy({ where: {} });
}
});
return Promise.all(promises);
};
Then in the test, I am doing this:
describe.only('application mutations', () => {
beforeEach(() => truncateTable());
...
The error I am getting:
SequelizeDatabaseError: could not serialize access due to concurrent
update
TL/DR: in your tests, if you want a quick way to delete models and reset your DB, use sync.
describe.only('application mutations', () => {
beforeEach(async () => {
await db.sync({force: true})
});
}
If you want to individually destroy your models, you must properly await for your promise to finish before initiating a new one. Currently, your promises are being initiated all at once, hence the Sequelize error.
export const truncateTable = async () => {
const promises = Object.keys(db).map(key => {
if (key !== 'Sequelize' && key !== 'sequelize') {
await db[key].destroy({ where: {} });
}
});
};
// in your test file
describe.only('application mutations', () => {
beforeEach(async () => {
await truncateTable();
});
})
I think I misunderstand how promise cancellation with bluebird works. I wrote a test that demonstrates this. How do I make it green? Thanks:
describe('cancellation tests', () => {
function fakeFetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 100);
});
}
function awaitAndAddOne(p1) {
return p1.then(res => res + 1);
}
it('`cancel` cancels promises higher up the chain', () => {
const p1 = fakeFetch();
const p2 = awaitAndAddOne(p1);
expect(p2.isCancellable()).toBeTruthy();
p2.cancel();
return p2
.then(() => {
console.error('then');
})
.catch(err => {
console.error(err);
})
.finally(() => {
expect(p2.isCancelled()).toBeTruthy(); // Expected value to be truthy, instead received false
expect(p1.isCancelled()).toBeTruthy();
});
});
});
From here:
The cancellation feature is by default turned off, you can enable it using Promise.config.
Seems like you didn't enable the cancellation flag on the Promise itself:
Promise.config({
cancellation: true
});
describe(...
#Karen if correct. But the issue is that your test is also a bit wrong
If you look at the isCancellable method
Promise.prototype.isCancellable = function() {
return this.isPending() && !this.isCancelled();
};
This is just checking if the promise is pending and is not already cancelled. This doesn't mean then cancellation is enabled.
http://bluebirdjs.com/docs/api/cancellation.html
If you see the above url, it quotes below
The cancellation feature is by default turned off, you can enable it using Promise.config.
And if you look at the cancel method
Promise.prototype["break"] = Promise.prototype.cancel = function() {
if (!debug.cancellation()) return this._warn("cancellation is disabled");
Now if I update your test correct like below
var Promise = require("bluebird");
var expect = require("expect");
describe('cancellation tests', () => {
function fakeFetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 100);
});
}
function awaitAndAddOne(p1) {
return p1.then(res => res + 1);
}
it('`cancel` cancels promises higher up the chain', () => {
const p1 = fakeFetch();
const p2 = awaitAndAddOne(p1);
value = p2.isCancellable();
expect(p2.isCancellable()).toBeTruthy();
p2.cancel();
expect(p2.isCancelled()).toBeTruthy(); // Expected value to be truthy, instead received false
expect(p1.isCancelled()).toBeTruthy();
});
});
You can see that cancellation is not enable and it executes the warning code
The execution results fail as expected
spec.js:46
cancellation tests
spec.js:46
1) `cancel` cancels promises higher up the chain
spec.js:78
0 passing (3m)
base.js:354
1 failing
base.js:370
1) cancellation tests
base.js:257
`cancel` cancels promises higher up the chain:
Error: expect(received).toBeTruthy()
Expected value to be truthy, instead received
false
at Context.it (test/index.test.js:37:36)
Now if you update the code to enable cancellation
var Promise = require("bluebird");
var expect = require("expect");
Promise.config({
cancellation: true
});
describe('cancellation tests', () => {
function fakeFetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 100);
});
}
function awaitAndAddOne(p1) {
return p1.then(res => res + 1);
}
it('`cancel` cancels promises higher up the chain', () => {
const p1 = fakeFetch();
const p2 = awaitAndAddOne(p1);
value = p2.isCancellable();
expect(p2.isCancellable()).toBeTruthy();
p2.cancel();
expect(p2.isCancelled()).toBeTruthy(); // Expected value to be truthy, instead received false
expect(p1.isCancelled()).toBeTruthy();
});
});
It works!
I have been playing with rxjs and redux-observable for the last few days and have been struggle to find a way to a test for Observable.ajax. I have the following epic which create a request to https://jsonplaceholder.typicode.com/,
export function testApiEpic (action$) {
return action$.ofType(REQUEST)
.switchMap(action =>
Observable.ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
)
}
where,
export const REQUEST = 'my-app/testApi/REQUEST'
export const SUCCESS = 'my-app/testApi/SUCCESS'
export const FAILURE = 'my-app/testApi/FAILURE'
export const CLEAR = 'my-app/testApi/CLEAR'
export function requestTestApi () {
return { type: REQUEST }
}
export function successTestApi (response) {
return { type: SUCCESS, response }
}
export function failureTestApi (error) {
return { type: FAILURE, error }
}
export function clearTestApi () {
return { type: CLEAR }
}
The code works fine when runs in browser but not when testing with Jest.
I have try,
1) Create a test based on https://redux-observable.js.org/docs/recipes/WritingTests.html. The store.getActions() returns only { type: REQUEST }.
const epicMiddleware = createEpicMiddleware(testApiEpic)
const mockStore = configureMockStore([epicMiddleware])
describe.only('fetchUserEpic', () => {
let store
beforeEach(() => {
store = mockStore()
})
afterEach(() => {
epicMiddleware.replaceEpic(testApiEpic)
})
it('returns a response, () => {
store.dispatch({ type: REQUEST })
expect(store.getActions()).toEqual([
{ type: REQUEST },
{ type: SUCCESS, response }
])
})
})
2) Create a test based on Redux-observable: failed jest test for epic. It returns with
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('returns a response', (done) => {
const action$ = ActionsObservable.of({ type: REQUEST })
const store = { getState: () => {} }
testApiEpic(action$, store)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
{ type: SUCCESS, response }
])
done()
})
})
Can someone point me out what is the correct way to test Observable.ajax ?
I would follow the second example, from StackOverflow. To make it work you'll need to make some minor adjustments. Instead of importing Observable.ajax in your epic file and using that reference directly, you need to use some form of dependency injection. One way is to provide it to the middleware when you create it.
import { ajax } from 'rxjs/observable/dom/ajax';
const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { ajax }
});
The object we passed as dependencies will be give to all epics as the third argument
export function testApiEpic (action$, store, { ajax }) {
return action$.ofType(REQUEST)
.switchMap(action =>
ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
);
}
Alternatively, you could not use the dependencies option of the middleware and instead just use default parameters:
export function testApiEpic (action$, store, ajax = Observable.ajax) {
return action$.ofType(REQUEST)
.switchMap(action =>
ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
);
}
Either one you choose, when we test the epic we can now call it directly and provide our own mock for it. Here are examples for success/error/cancel paths These are untested and might have issues, but should give you the general idea
it('handles success path', (done) => {
const action$ = ActionsObservable.of(requestTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.of({ url, method })
};
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
successTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
])
done();
});
});
it('handles error path', (done) => {
const action$ = ActionsObservable.of(requestTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.throw({ url, method })
};
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
failureTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
])
done();
});
});
it('supports cancellation', (done) => {
const action$ = ActionsObservable.of(requestTestApi(), clearTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.of({ url, method }).delay(100)
};
const onNext = chai.spy();
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe({
next: onNext,
complete: () => {
onNext.should.not.have.been.called();
done();
}
});
});
For the first way:
First, use isomorphic-fetch instead of Observable.ajax for nock support, like this
const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
.then(res => res.json());
return Observable.from(request);
};
So my epic is:
const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
action$
.ofType(FETCH_MODEL)
.mergeMap((action: FetchModel) =>
fetchDynamicData(action.url, action.params)
.map((payload: FetchedData) => fetchModelSucc(payload.data))
.catch(error => Observable.of(
fetchModelFail(error)
)));
Then, you may need an interval to decide when to finish the test.
describe("epics", () => {
let store: MockStore<{}>;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(epic);
});
it("fetch data model succ", () => {
const payload = {
code: 0,
data: someData,
header: {},
msg: "ok"
};
const params = {
data1: 100,
data2: "4"
};
const mock = nock("https://test.com")
.get("/test")
.query(params)
.reply(200, payload);
const go = new Promise((resolve) => {
store.dispatch({
type: FETCH_MODEL,
url: "https://test.com/test",
params
});
let interval: number;
interval = window.setInterval(() => {
if (mock.isDone()) {
clearInterval(interval);
resolve(store.getActions());
}
}, 20);
});
return expect(go).resolves.toEqual([
{
type: FETCH_MODEL,
url: "https://test.com/assignment",
params
},
{
type: FETCH_MODEL_SUCC,
data: somData
}
]);
});
});
enjoy it :)