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
}));
});
});
Related
Test case works in Postman, but not from Visual Studio Code or Jest command line.
Request Body in Postman that works and returns all 4 errors:
{ "Item": {} }
This is missing the FileName and Item.Data fields.
API.dto.ts
import { Type } from 'class-transformer';
import { IsNumberString, IsString, MinLength, ValidateNested } from 'class-validator';
// Expected Payload
// {
// FileName: 'abc',
// Item: {
// Data: '123'
// }
// }
export class ItemDataDTO {
#IsNumberString() #MinLength(2) public readonly Data: string;
}
/**
* This class is the Data Object for the API route
*/
export class ApiDTO {
#IsString() #MinLength(1) public readonly FileName: string;
#ValidateNested()
#Type(() => ItemDataDTO)
Item: ItemDataDTO;
}
api.controller.ts
import { Body, Controller, Get, Options, Put, Request, Response } from '#nestjs/common';
import { ApiDTO } from './api.dto';
#Controller('api')
export class ApiController {
constructor() { }
#Put('donotuse')
public DoNotUse(#Body() APIBody: ApiDTO) {
return 'OK';
}
}
API.dto.spec.ts
import { ArgumentMetadata, ValidationPipe } from '#nestjs/common';
import { ApiDTO } from './api.dto';
describe('ApiDto', () => {
it('should be defined', () => {
expect(new ApiDTO()).toBeDefined();
});
it('should validate the ApiDTO definition', async () => {
const target: ValidationPipe = new ValidationPipe({
transform: true,
whitelist: true,
});
const metadata: ArgumentMetadata = {
type: 'body',
metatype: ApiDTO,
data: '{ "Item": {} }',
};
const Expected: string[] = [
'FileName must be longer than or equal to 1 characters',
'FileName must be a string',
'Item.Data must be longer than or equal to 2 characters',
'Item.Data must be a number string',
];
await target.transform(<ApiDTO>{}, metadata).catch((err) => {
expect(err.getResponse().message).toEqual(Expected);
});
});
});
The expect fails.
await target.transform(<ApiDTO>{}, metadata).catch((err) => {
expect(err.getResponse().message).toEqual(Expected);
});
The 2 FileName errors are returned, but no the Item.Data fields. Setting data: '', to be data: '{ "Item": {} }' also fails the same way.
Actual expectation failure:
expect(received).toEqual(expected) // deep equality
- Expected - 2
+ Received + 0
Array [
"FileName must be longer than or equal to 1 characters",
"FileName must be a string",
- "Item.Data must be longer than or equal to 2 characters",
- "Item.Data must be a number string",
]
This is indicating that the FileName validation is there, those 2 lines are returned, but the Item.Data errors, are not coming back, and are 'extra' in my test case results.
However, calling this via Postman, PUT /api/donotuse with the request body:
{ "Item": {} }
returns all 4 of the errors. The HTTP Status code is also a 400 Bad Request, as NestJS would normally return on its own. I am not sure what is wrong in my test case to get the errors to all be returned.
EDIT
I have also then tried to do this via E2E testing as the answer suggested, but I still receive the same missing errors.
describe('ApiDto - E2E', () => {
let app: INestApplication;
afterAll(async () => {
await app.close();
});
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }));
await app.init();
});
it('should validate the ApiDTO definition', async () => {
const APIRequestDTO: unknown = { FileName: null, Item: {} };
const ResponseData$ = await request(app.getHttpServer())
.put('/api/donotuse')
.set('Accept', 'application/json')
.send(APIRequestDTO as ApiDTO);
const Expected: string[] = [
'FileName must be longer than or equal to 1 characters',
'FileName must be a string',
'Item.Data must be longer than or equal to 2 characters',
'Item.Data must be a number string',
];
expect(ResponseData$.status).toBe( HttpStatus.UNPROCESSABLE_ENTITY);
expect(ResponseData$.body.message).toBe(Expected);
});
});
This still does not provide all the errors, that are properly returned from the Postman call. I am not sure what is happening during testing that the sub type is not processed. Calling this via Postman, same body, same headers, etc., does return the proper errors:
"message": [
"FileName must be longer than or equal to 1 characters",
"FileName must be a string",
"Item.Data must be longer than or equal to 2 characters",
"Item.Data must be a number string"
],
I know it is going into the ValidationPipe as well, as my custom error code, 422 Unprocessable Entity is returned, indicating this is the validation that is failing. This same error is returned in both my unit test and the E2E test, but not the second set of errors about Item.Data.
I assume that in your app you're registering the ValidationPipe globally, eg:
app.useGlobalPipes(new ValidationPipe());
Due to the location of where Global Pipes are registered, they will work when you execute an actual request against your backend but will not be picked up in tests. This is why you're seeing it working through Postman, but not through Jest.
If you want the Validation pipe to be used in your tests you will need to manually set it up like so:
// Probably in your beforeEach where you're setting up the test module
const app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
Duplicate of How to apply Global Pipes during e2e tests
I have written a pipe that filters an input observable. In the pipe I specify a timeout with the timeout() operator to abort waiting if the expected value is not emitted by the source in time.
I want to test the timeout case with jasmine-marbles, but I can't get it to work.
I believe that expect(source).toBeObservable() evaluates before the source emits.
see Stackblitz
The pipe to be tested:
source = cold('a', { a: { id: 'a' } }).pipe(
timeout(500),
filter((a) => false),
catchError((err) => {
return of({ timeout: true })
}),
take(1)
);
Testing with toPromise() works as expected:
expect(await source.toPromise()).toEqual({ timeout: true });
Testing with jasmine-marbles
const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);
fails with the error
Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 500, notification: Notification({ kind: 'N', value: Object({ timeout: true }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 500, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).
Support for time progression was recently added (see jasmine-marbles PR #38) to jasmine-marbles 0.5.0. Additional test specs were added to the package that demonstrate one of a couple of possible ways to accomplish what you want. Here are some options I was able to throw together using your Stackblitz sample.
Option 1
When you initialize the source observable outside the test method (e.g. in beforeEach), you must explicitly initialize and pass the test scheduler to timeout to get expect().toBeObservable() working. However, take note that this change will break the "should work with toPromise" test. (I don't know why, but toPromise() doesn't appear to work with this approach.)
describe('Marble testing with timeout', () => {
let source;
beforeEach(() => {
// You must explicitly init the test scheduler in `beforeEach`.
initTestScheduler()
source = cold('a', { a: { id: 'a' } }).pipe(
// You must explicitly pass the test scheduler.
timeout(500, getTestScheduler()),
filter((a) => false),
catchError(err => {
return of({ timeout: true })
}),
take(1)
);
});
it('should work with toBeObservable', () => {
const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);
});
});
Option 2
You can refactor things slightly and initialize the source observable inside the test method (not in beforeEach). You don't need to explicitly initializes the test scheduler (jasmine-marbles will do it for you before the test method runs), but you still have to pass it to timeout. Note how the createSource function can be used with the test scheduler or the default scheduler (if the scheduler argument is left undefined). This options works with both the "should work with toPromise" test and the "should work with toBeObservable" test.
describe('Marble testing with timeout', () => {
const createSource = (scheduler = undefined) => {
return cold('a', { a: { id: 'a' } }).pipe(
// You must explicitly pass the test scheduler (or undefined to use the default scheduler).
timeout(500, scheduler),
filter((a) => false),
catchError(err => {
return of({ timeout: true })
}),
take(1)
);
};
it('should work with toPromise', async () => {
const source = createSource();
expect(await source.toPromise()).toEqual({ timeout: true });
});
it('should work with toBeObservable', () => {
const source = createSource(getTestScheduler());
const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);
});
});
Option 3
Finally, you can skip passing the test scheduler to timeout if you explicitly use the test scheduler's run method, but you must use expectObservable (as opposed to expect().toBeObservable(). It works just fine, but Jasmine will report the warning "SPEC HAS NO EXPECTATIONS".
describe('Marble testing with timeout', () => {
let source;
beforeEach(() => {
source = cold('a', { a: { id: 'a' } }).pipe(
timeout(500),
filter((a) => false),
catchError(err => {
return of({ timeout: true })
}),
take(1)
);
});
it('should work with scheduler and expectObservable', () => {
const scheduler = getTestScheduler();
scheduler.run(({ expectObservable }) => {
expectObservable(source).toBe('500ms (0|)', [{ timeout: true }]);
});
});
});
I have an arrow function in my React-Redux application that dispatches just an action without a payload, which will wipe (Reset) a part of my Redux store. There's no HTTP (or Promises) involved.
export const resetSearch = () => dispatch => {
dispatch({
type: RESET_SEARCH
});
};
I want to test that it returns the correct Action type:
import * as actions from '../actions/actionCreators';
import * as types from '../actions/actionTypes';
describe('actions', () => {
it('should create an action to reset part of the store', () => {
const expectedAction = {
type: types.RESET_SEARCH
};
expect(actions.resetSearch()).toEqual(expectedAction);
});
});
The test is not passing because I need to return an Object but, instead, I send this anonimous arrow function. Here is the Jest output
Expected value to equal: {"type": "RESET_SEARCH"}
Received: [Function anonymous]
How should the test be?
All help will be apreaciated!
Thanks
Can you plese try below code snippet, It should do:
const expectedAction = {
type: types.RESET_SEARCH
};
let retnFunc = actions.resetSearch();
retnFunc((receivedAction)=>{
expect(receivedAction).toEqual(expectedAction);
});
Here resetSearch returns a function which gets called with the action object so just imitated the same.
Let me know if you need any more help!
So I've run into another snag, which I'm fighting with... I have a method that is a sync call, and within this method it calls a promise, async, method.
in my app I have the following:
export class App {
constructor(menuService) {
_menuService = menuService;
this.message = "init";
}
configureRouter(config, router) {
console.log('calling configureRouter');
_menuService.getById(1).then(menuItem => {
console.log('within then');
console.log(`configureRouter ${JSON.stringify(menuItem, null, 2)}`);
const collection = menuItem.links.map(convertToRouteCollection);
console.log(`collection ${JSON.stringify(collection, null, 2)}`);
//I think there is an issue with asyn to synch for the test
config.map(collection);
}).catch(err => {
console.error(err);
});
console.log('calling configureRouter assign router');
this.router = router;
}
}
The test I've tried the following within mocha
...
it('should update router config', function () {
const expectedData = {
name: "main menu",
links: [{
url: '/one/two',
name: 'link name',
title: 'link title'
}]
};
const configMapStub = sinon.stub();
const config = {
map: configMapStub
};
const routerMock = sinon.stub();
let app = null;
const actualRouter = null;
let menuService = null;
setTimeout(() => {
menuService = {
getById: sinon.stub().returns(Promise.resolve(expectedData).delay(1))
};
app = new App(menuService);
app.configureRouter(config, routerMock);
}, 10);
clock.tick(30);
expect(app.router).to.equal(routerMock);
expect(menuService.getById.calledWith(1)).to.equal(true);
//console.log(configMapStub.args);
expect(configMapStub.called).to.equal(true);
const linkItem = expectedData.links[0];
const actual = [{
route: ['', 'welcome'],
name: linkItem.name,
moduleId: linkItem.name,
nav: true,
title: linkItem.title
}];
console.log(`actual ${JSON.stringify(actual, null, 2)}`);
expect(config.map.calledWith(actual)).to.equal(true);
});
...
No matter what, I get configMockStub to always get false, while I am getting the menuService.getById.calledWith(1).to.equal(true) to equal true.
The test above was an attempt to try and get 'time' to pass. I've tried it without and have equally failed.
I'm really striking out on ideas on how to test this. Maybe I have the code wrong to reference a promise inside this method.
The only thing I can say I don't have any choice over the configureRouter method. Any guidance is appreciated.
Thanks!
Kelly
Short answer:
I recently discovered I was trying to make configureRouter method be a synchronous call (making it use async await keywords). What I found out was Aurelia does allow that method to be promised. Because of this, the test in question is no longer an issue.
Longer answer:
The other part of this is that I had a slew of babel issues lining up between babelling for mocha, and then babelling for wallaby.js. For some reason these two were not playing well together.
in the test above, another thing was to also change the following:
it('should update router config', function () {
to
it('should update router config', async function () {
I feel like there was another step, but at this time I cannot recall. In either case, knowing that I could use a promise made my world much easier for Aurelia.
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.