How can I create parametrized tests with Mocha?
Sample use case: I have 10 classes, that are 10 different implementations of the same interface. I want to run the exact same suit of tests for each class. I can create a function, that takes the class as a parameter and runs all tests with that class, but then I will have all tests in a single function - I won't be able to separate them nicely to different "describe" clauses...
Is there a natural way to do this in Mocha?
You do not need async package. You can use forEach loop directly:
[1,2,3].forEach(function (itemNumber) {
describe("Test # " + itemNumber, function () {
it("should be a number", function (done) {
expect(itemNumber).to.be.a('number')
expect(itemNumber).to.be(itemNumber)
});
});
});
I know this was posted a while ago but there is now a node module that makes this really easy!! mocha param
const itParam = require('mocha-param').itParam;
const myData = [{ name: 'rob', age: 23 }, { name: 'sally', age: 29 }];
describe('test with array of data', () => {
itParam("test each person object in the array", myData, (person) => {
expect(person.age).to.be.greaterThan(20);
})
})
Take a look at async.each. It should enable you to call the same describe, it, and expect/should statements, and you can pass in the parameters to the closure.
var async = require('async')
var expect = require('expect.js')
async.each([1,2,3], function(itemNumber, callback) {
describe('Test # ' + itemNumber, function () {
it("should be a number", function (done) {
expect(itemNumber).to.be.a('number')
expect(itemNumber).to.be(itemNumber)
done()
});
});
callback()
});
gives me:
$ mocha test.js -R spec
Test # 1
✓ should be a number
Test # 2
✓ should be a number
Test # 3
✓ should be a number
3 tests complete (19 ms)
Here's a more complex example combining async.series and async.parallel: Node.js Mocha async test doesn't return from callbacks
Actually the mocha documentation specifies how to create what you want here
describe('add()', function() {
var tests = [
{args: [1, 2], expected: 3},
{args: [1, 2, 3], expected: 6},
{args: [1, 2, 3, 4], expected: 10}
];
tests.forEach(function(test) {
it('correctly adds ' + test.args.length + ' args', function() {
var res = add.apply(null, test.args);
assert.equal(res, test.expected);
});
});
});
The answer provided Jacob is correct just you need to define the variable first before iterating it.
Related
I have found that expect() works within describe(). So the function test() is not necessary, if I see it right. Do I see it right?
In other words, is it enough to write that:
const sum = require('./sum');
describe('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
Or does that bring advantages:
const sum = require('./sum');
describe('test sum', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
describe(name, fn) creates a block that groups together several
related tests.
Detail: https://jestjs.io/docs/api#describename-fn
I am trying to test AsyncTypeahead from react-bootstrap-typeahead.
I have a very simple test component :
class AsyncTypeahead2 extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
};
}
render() {
return ( <AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/> )
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id:1, stateName:"Alaska"},
{id:2, stateName:"Alabama"}
]
},
);
(Note that the URL is mocked to return two elements)
When I run this in my storybook it looks fine :
But if I want to test it (with Enzyme) it does not recognise the < li > items that pop up.
let Compoment =
<div>Basic AsyncTypeahead Example
<AsyncTypeahead2/>
</div>
const wrapper = mount(Compoment);
let json = wrapper.html();
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
Instead of finding two "dropdown-item" items there is just one item with the text "Type to Search...".
Is the AynchTypeahead not updating the DOM correctly with respect to Enzyme?
<AsyncTypeahead> is asynchronous. On the other hand simulate() is synchronous. So at the time you get to expect() AsyncTypeahead not even started to populate the dropdown with <li> elements. You need to wait for it.
It's not specified, but it looks like you are using fetch-mock package.
There is the flush function which
Returns a Promise that resolves once all fetches handled by fetch-mock have resolved
So this:
...
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
await fetchMock.flush() // !!!
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2)
should work.
...But probably it won't. Because
fetchMock.mock(...)
fetch(...)
await fetchMock.flush()
does work, but
fetchMock.mock(...)
setTimeout(() => fetch(...), 0)
await fetchMock.flush()
does not. await fetchMock.flush() returns right away if there was no call of fetch. And probably there won't be. Because <AsyncTypeahead> debounces.
(By the way, you can also try to mock fetch on a per-test basis. Just in case.)
So I see two options:
Use something else instead of fetch-mock package. Where you can resolve your own Promises on mocked requests completion.
https://tech.travelaudience.com/how-to-test-asynchronous-data-fetching-on-a-react-component-ff2ee7433d71
import waitUntil from 'async-wait-until';
...
test("test name", async () => {
let Compoment = <AsyncTypeahead2/>
...
await waitUntil(() => wrapper.state().isLoading === false);
// or even
// await waitUntil(() => wrapper.find(".dropdown-item").length === 2, timeout);
expect(...)
})
This options if not pretty. But maybe it's your only option - there is not only the fetch-mock you should worry about. setState also asynchronous... and it looks like there is no pretty way to check when it's done updating the state and the DOM without changing the real code (which is quite undesirable).
The exact solution to my problem is in the following code (copy and paste into a JS file to see it work).
Things to note :
I needed to use the waitUntil function from the async-wait-until library. fetch-mock on its own does not provide the functionality to test async code.
I needed to add an ugly hack at global.document.createRange because of some tooltip issue with react-bootstrap-typeahead and jest.
use waitUntil to wait on changes on the internal state of the component
It is very important to call wrapper.update() to update the DOM afterwards.
..
import React, {Component} from 'react';
import waitUntil from 'async-wait-until';
import {mount} from "enzyme";
import fetchMock from "fetch-mock";
import {AsyncTypeahead} from "react-bootstrap-typeahead";
describe('Autocomplete Tests ', () => {
test(' Asynch AutocompleteInput ', async () => {
class AsyncTypeaheadExample extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
finished: false
};
}
render() {
return (<AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
finished: true
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/>)
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id: 1, stateName: "Alaska"},
{id: 2, stateName: "Alabama"}
]
},
);
let Compoment =
<AsyncTypeaheadExample/>
// ugly hacky patch to fix some tooltip bug
// https://github.com/mui-org/material-ui/issues/15726
global.document.createRange = () => ({
setStart: () => {
},
setEnd: () => {
},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
let wrapper = mount(Compoment);
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', {target: {value: "al"}});
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
//now the async stuff is happening ...
await waitUntil(() => {
return wrapper.state().finished === true;
}, 3000); //wait about 3 seconds
wrapper.update() //need to update the DOM!
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
})
});
UPDATE
I can also compare on wrapper items rather than doing a direct comparison on the state :
//now the async stuff is happening ...
await waitUntil(() => {
wrapper.update() //need to update the DOM!
return wrapper.find(".dropdown-item").length > 1
}, 3000); //wait about 3 seconds
This is probably better because it means i dont need to know about the component internals.
I'm trying to write tests for the following function that uses retryWhen operator:
// some API I'm using and mocking out in test
import { geoApi } from "api/observable";
export default function retryEpic(actions$) {
return actions$.pipe(
filter(action => action === 'A'),
switchMap(action => {
return of(action).pipe(
mergeMap(() => geoApi.ipLocation$()),
map(data => ({ data })),
retryWhen(errors => {
return errors.pipe(take(2));
}),
);
}),
);
}
The code is supposed to perform a request to some remote API geoApi.ipLocation$(). If it gets an error, it retries 2 times before giving up.
I have written the following test code that uses Jest and RxJS TestScheduler:
function basicTestScheduler() {
return new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
}
const mockApi = jest.fn();
jest.mock('api/observable', () => {
return {
geoApi: {
ipLocation$: (...args) => mockApi(...args),
},
};
});
describe('retryEpic()', () => {
it('retries fetching 2 times before succeeding', () => {
basicTestScheduler().run(({ hot, cold, expectObservable, expectSubscriptions }) => {
const actions$ = hot('-A');
// The first two requests fail, third one succeeds
const stream1 = cold('-#', {}, new Error('Network fail'));
const stream2 = cold('-#', {}, new Error('Network fail'));
const stream3 = cold('-r', { r: 123 });
mockApi.mockImplementationOnce(() => stream1);
mockApi.mockImplementationOnce(() => stream2);
mockApi.mockImplementationOnce(() => stream3);
expectObservable(retryEpic(actions$)).toBe('----S', {
S: { data: 123 },
});
expectSubscriptions(stream1.subscriptions).toBe('-^!');
expectSubscriptions(stream2.subscriptions).toBe('--^!');
expectSubscriptions(stream3.subscriptions).toBe('---^');
});
});
});
This test fails.
However, when I replace retryWhen(...) with simply retry(2), then the test succeeds.
Looks like I don't quite understand how to implement retry with retryWhen. I suspect this take(2) is closing the stream and kind of preventing everything from continuing. But I don't quite understand it.
I actually want to write some additional logic inside retryWhen(), but first I need to understand how to properly implement retry() with retryWhen(). Or perhaps that's actually not possible?
Additional resources
My implementation of retryWhen + take was based on this SO answer:
How to create an RXjs RetryWhen with delay and limit on tries
Official docs:
retryWhen
You can use retryWhen for those two purposes, one to have your logic in it and the second is the retry numbers you'd like to give it (no need to use retry operator):
// some API I'm using and mocking out in test
import { geoApi } from "api/observable";
export default function retryEpic(actions$) {
return actions$.pipe(
filter(action => action === 'A'),
switchMap(action => {
return of(action).pipe(
mergeMap(() => geoApi.ipLocation$()),
map(data => ({ data })),
retryWhen(errors =>
errors.pipe(
mergeMap((error, i) => {
if (i === 2) {
throw Error();
}
// return your condition code
})
)
)
)
}),
);
}
Here is a simple DEMO of that.
As for understanding this logic:
retryWhen and retry operators, according to the Official docs you've referenced:
resubscribing to the source Observable (if no error or complete executes)
This is why you can't pipe retry and retryWhen together. You can say that these operators are a chain breakers...
So I'm trying to test a Subject's behavior and it's not working, and it seems like there some things I'm not understanding correctly. Consider the following test:
it('marble testing subject test', () => {
const obs$: Subject<number> = new Subject<number>();
obs$.next(42);
obs$.next(24);
expect(obs$.asObservable()).toBeObservable(hot('xy', { x: 42, y: 24 }));
});
This fails with this error message:
Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 0, notification: Notification({ kind: 'N', value: 42, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: 24, error: undefined, hasValue: true }) }).
I think I sort of understand why: the Subject (per documentation) only emits values after the start of the subscription. The toBeObservable() function (I'm assuming) subscribes to the Subject and so my 2 next calls happened before this, so they won't emit.
So my question is, how would I test something like this? i.e. testing a series of emissions from a subject over time? Is this possible to do with marble testing? I can sort of get it to work by changing it out for a ReplaySubject but if I do, the marble diagram has to be (xy) instead of xy.
Thanks.
I have this working in the context of an Angular application
My service, the key part being that I define my things as a getter, giving me a chance to actually spy on asObservable if I just defined things$ as a property then the spy doesn't work:
#Injectable({
providedIn: 'root'
})
export class ApiService {
private things = new BehaviorSubject<Array<string>>([]);
public get things$(): Observable<boolean> {
return this.things.asObservable().pipe(map((things) => things.length > 0))
}
}
And then in my test, I guess the key part is that I am spying on the asObservable method on the things BehaviourSubject. Like this:
describe('#things$', () => {
it('should ', () => {
const things. = 'a-b-a';
const expected = 't-f-t';
// spy on
spyOn(service['things'], 'asObservable').and.returnValue(hot(things, {
a: ['a', 'b'],
b: []
}));
expect(service.things$).toBeObservable(cold(expected, {
t: true,
f: false
}));
});
});
Okay as a C# NUnit guy this might be odd.
But does jasmine allow parameterized unit test?
I am not sure if it goes against the "declare" and "it" to make things readable to non programmers.
I have seen some third party plug ins but they are kind of old, not sure if it has been added to jasmine.
If I am ment to use a plug in
Just to help anyone who finds this in the future, I have been told on jasmine forum There is no first class support for parameterized tests within Jasmine itself.
Based on piotrek's answer and the article Parameterized testing in Javascript, you could also use the following approach which uses ES6 syntax:
[
['abc', 3],
['ab', 2],
['', 0],
].forEach(([string, expectedLength]) => {
it(`should return length ${expectedLength} for string "${string}"`, () => {
expect(string.length).toBe(expectedLength);
});
});
I have tested it with the Jest test framework, but it should work with Jasmine as well.
Better solution (especially if you use TypeScript)
Another solution is to use Array of Objects instead of Array of Arrays. It fits better if you use some typing system like TypeScript.
Type issue
Imagine you have the following parametrised test:
it('action(value) should reset the forms pool only if value is true', () => {
[
[true, 1],
[false, 0],
].forEach(([value, calledTimes]) => {
spyResetFormsPool.calls.reset();
component.action(value); // type error #1
expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes); // type error #2
});
});
with TypeScript, it will fail to compile giving two errors:
error #1:
error TS2345: Argument of type 'number | boolean' is not assignable to parameter of type 'boolean'.
error #2:
error TS2345: Argument of type 'number | boolean' is not assignable to parameter of type 'number'. Type 'true' is not
assignable to type 'number'.
That is because TypeScript sees an array of 'number | boolean'.
We could quickly solve this warning by using some explicit cast:
it('action(value) should reset the forms pool only if value is true', () => {
[
[true, 1],
[false, 0],
].forEach(([value, calledTimes]) => {
spyResetFormsPool.calls.reset();
component.action(value as boolean); // necessary cast
expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes as number); // necessary cast
});
});
however this solution is not very nice.
Solution
A better way is to use Array of Objects, so the types are correctly handled by default and there is no need of explicit casting:
it('action(value) should reset the forms pool only if value is true', () => {
[
{ value: true, calledTimes: 1 },
{ value: false, calledTimes: 0 },
].forEach(({ value, calledTimes }) => {
spyResetFormsPool.calls.reset();
component.action(value);
expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes);
});
});
Do you want to use for instead of forEach (I personally find it more readable)? That's also possible:
it('action(value) should reset the forms pool only if value is true', () => {
for (const {value, calledTimes} of [
{value: true, calledTimes: 1},
{value: false, calledTimes: 0},
]) {
spyResetFormsPool.calls.reset();
component.action(value);
expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes);
}
});
Alternatively, you can also move the it inside the loop. When I do this, I usually add a testId to each object so I can keep track of which tests are failing:
for (const {value, calledTimes} of [
{ testId: 1, value: true, calledTimes: 1 },
{ testId: 2, value: false, calledTimes: 0 },
]) {
it(`action(value) should reset the forms pool only if value is true [${testId}]`, () => {
spyResetFormsPool.calls.reset();
component.action(value);
expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes);
});
}
You can use the following convention to increase readability:
const testCases = [
{actualValue: true, expectedValue: true},
{actualValue: false, expectedValue: false}
]
testCases.forEach(({actualValue, expectedValue}) => {
it(`should be the same given: ${actualValue} and expected :${expectedValue} values`, () => {
expect(actualValue).toBe(expectedValue)
})
})
You'll see the following test cases to run:
Test Results
+ should be the same given: true and expected: true values
+ should be the same given: false and expected: false values
i haven't worked with jasmine since a long time but it was pretty easy to add parameterized tests:
['abc', 3,
'ab', 4,
'', 0].
it('should contain string length', function(string, expected){
expect(string.length).toBe(expected);
});
with just a few lines of infrastructure code:
Array.prototype.it = function(description, testCaseFunction) {
_(this)
.chunk(testCaseFunction.length)
.each(function(innerArray){
it(description + ' ' + JSON.stringify(innerArray), function(){
testCaseFunction.apply(this, innerArray);
});
})
.value();
};
depending on your desired syntax and willingness to change default js objects, you have plenty of options: http://blog.piotrturski.net/2015/04/jasmine-parameterized-tests.html
So I started combining:
YAML using js-yaml
jasminejs
Typescript
to create what I believe are readable parameterized tests like this:
import YamlTableReader, {fixtureData, TestData} from "./YamlTableReader";
describe("TestSuite", () => {
describe("Real TestCase with Data Fixture", () => {
// now using tagged template-string to auto convert into YamlTableReader.
var testdata = fixtureData `
| ID | Value1 | Value2 | Squared |
| 0 |1 | 1 | 1 |
| 1 |2 | 2 | 4 |
| 2 |3 | 3 | 91 |
`;
// This actually creates a test for each row of the table above
testdata.describeEach("Square Test","[ID={ID}]:{Value1} x {Value2} should be equal to {Squared}",
(row: {Value1: number, Value2: number, Squared: number}) => {
expect((row.Value1 * row.Value2)).toBe(row.Squared)
}
);
});
Running this will give the following results:
Failures:
1) TestSuite 2 Real TestCase with Data Fixture Square Test : [ID=2]:3 x 3 should be equal to 91
Message:
Expected 9 to be 91.
Sources: https://github.com/deicongmbh/jasmine-param-tests