I'm a long time tape.js user and I'm working on learning how to work jest. I'm interested in providing descriptions for each my test cases as part of the assertion, ala this tape test
function myCoolTest(t) {
t.equal('batman'.length, 6, 'batman should have the right number of characters in it');
t.ok(1 === 1, 'basic truths should stay true');
t.deepEqual({test: 1}, {test: 1}, 'deep equality of objects works sensibly');
t.end();
}
I like being able to annotate my tests (eg 'batman should have the right number of characters in it'), that way as I'm reading the output it's clear whats passed and what's failed. As far as I can tell the jest equivalent is
test('example test', () => {
expect('batman'.length).toBe(6);
expect(1 === 1).toBeTruthy();
expect({test: 1}).toBe({test: 1});
});
Which totally lacks description found in the first? While that's okay for simple examples like ^. The examples i've seen other places seem to suggest that if I want description I should add comments next to the relevant test, but this seems to prevent creating utility tests, eg
const expectEqual = (a: string, b: string): void =>
expect(JSON.parse(a)).toEqual(JSON.parse(b));
Am i just out of luck or are there methods that I am missing?
Related
I have some tests that accurately test my code's expected behavior, but that are rather brittle because I'm using literals. I'd like to make broader assertions about the shape of something. For example:
expect(mockedModule.mockFunction).toHaveBeenCalledTimes(1);
expect(mockedModule.mockFunction).toHaveLastBeenCalledWith(
"argument1",
{
field1: expect.stringMatching(/^argument1/), // but I also care about several other string matches
// and I _do not_ care about the order.
},
);
I could obviously repeatedly call toHaveLastBeenCalledWith with expect.any() littered through my code. But that's not very succinct, and it's not a great way to express the invariants I'm expecting.
Ideally I'd have something like:
expect(mockedModule.mockFunction).toHaveLastBeenCalledWith(
"argument1",
{
field1: expect.all(
expect.stringMatching(/^argument1/),
expect.any(
expect.stringMatching("42"),
expect.stringMatching("8675309"),
)
)
},
)
or the equivalent:
expect(mockedModule.mockFunction).toHaveLastBeenCalledWith(
"argument1",
{
field1: expect.all(
expect.stringMatching(/^argument1/).and(
expect.stringMatching("42").or(
expect.stringMatching("8675309)
)
),
)
},
)
But I have not found any functionality similar to this. This is most important for toHaveBeenCalledWith calls, where I never get a copy of the actual value, but have to create a matcher.
I have written a piece of software that parses and formats the fourth parameter of a graphql resolver function (the info object) to be used elsewhere. I would like to write unit tests for this software. Specifically, I do not want to build the GraphQLResolveInfo object myself, because doing that would be very cumbersome, error-prone and hard to maintain. Instead, I want to write human-readable query strings and convert them to GraphQLResolveInfo objects so I can pass those to my software.
After extensive googling and reading of the graphql-js source code, I have not found a simple way to do what they are doing internally. I'm really hoping that I am missing something.
What I am not trying to do is use the graphql-tag library, because that just generates an AST which has a very different format from the GraphQLResolveInfo type.
Has anyone done this before? Help would be much appreciated!
I will keep monitoring this question to see if a better answer comes along, but I've finally managed to solve my particular issue by creating as close an approximation of the GraphQLResolveInfo object as I need for my particular use case.
The GraphQLResolveInfo object is composed of several attributes, two of which are called fieldNodes and fragments. Both are in fact parts of the same AST that graphql-tag generates from a query string. These are the only parts of the GraphQLResolveInfo object that concern the software I wrote, the rest of it is ignored.
So here is what I did:
import gql from 'graphql-tag';
// The converter function
const convertQueryToResolveInfo = (query) => {
const operation = query.definitions
.find(({ kind }) => kind === 'OperationDefinition');
const fragments = query.definitions
.filter(({ kind }) => kind === 'FragmentDefinition')
.reduce((result, current) => ({
...result,
[current.name.value]: current,
}), {});
return {
fieldNodes: operation.selectionSet.selections,
fragments,
};
};
// An example call
const query = gql`
query {
foo {
bar
}
}
`;
const info = convertQueryToResolveInfo(query);
From the AST generated by graphql-tag, I extract and modify the operation and fragment definitions so that they look the way they do within the GraphQLResolveInfo object. This is by no means perfect and may be subject to change in the future depending on how my software evolves, but it is a relatively brief solution for my particular problem.
I'm using Enzyme with enzyme-to-json to do Jest snapshot testing of my React components. I'm testing shallow snapshots of a DateRange component that renders a display field with the current range (e.g. 5/20/2016 - 7/18/2016) and two DateInput components that allow selecting a Date value. This means that my snapshot contains the Dates I pass to the component both in the DateInput props and in a text representation it resolves itself. In my test I'm creating some fixed dates using new Date(1995, 4, 23).
When I run my test in different timezones, this produces different snapshots, because the Date(year, month, ...) constructor creates the date in the local timezone. E.g. use of new Date() produces this difference in snapshot between runs in my local timezone and on our CI server.
- value={1995-05-22T22:00:00.000Z}
+ value={1995-05-23T00:00:00.000Z}
I tried removing the timezone offset from the dates, but then the snapshot differed in the display field value, where the local timezone-dependent representation is used.
- value={5/20/2016 - 7/18/2016}
+ value={5/19/2016 - 7/17/2016}
How can I make my tests produce the same Dates in snapshots regardless of the timezone they're run in?
I struggled with this for hours/days and only this worked for me:
1) In your test:
Date.now = jest.fn(() => new Date(Date.UTC(2017, 7, 9, 8)).valueOf())
2) Then change the TZ env var before running your tests.
So the script in my package.json:
(Mac & Linux only)
"test": "TZ=America/New_York react-scripts test --env=jsdom",
(Windows)
"test": "set TZ=America/New_York && react-scripts test --env=jsdom",
I ended up with a solution comprised of two parts.
Never create Date objects in tests in timezone-dependent manner. If you don't want to use timestamps directly to have readable test code, use Date.UTC, e.g.
new Date(Date.UTC(1995, 4, 23))
Mock the date formatter used to turn Dates into display values, so that it returns a timezone-independent representation, e.g. use Date::toISOString(). Fortunately this was easy in my case, as I just needed to mock the formatDate function in my localization module. It might be harder if the component is somehow turning Dates into strings on its own.
Before I arrived at the above solution, I tried to somehow change how the snapshots are created. It was ugly, because enzyme-to-json saves a local copy of toISOString(), so I had to use _.cloneDeepWith and modify all the Dates. It didn't work out for me anyway, because my tests also contained cases of Date creation from timestamps (the component is quite a bit more complicated than I described above) and interactions between those and the dates I was creating in the tests explicitly. So I first had to make sure all my date definitions were referring to the same timezone and the rest followed.
Update (11/3/2017): When I checked enzyme-to-json recently, I haven't been able to find the local saving of toISOString(), so maybe that's no longer an issue and it could be mocked. I haven't been able to find it in history either though, so maybe I just incorrectly noted which library did it. Test at your own peril :)
I did this by using timezone-mock, it internally replaces the global Date object and it's the easiest solution I could find.
The package supports a few test timezones.
import timezoneMock from 'timezone-mock';
describe('when in PT timezone', () => {
beforeAll(() => {
timezoneMock.register('US/Pacific');
});
afterAll(() => {
timezoneMock.unregister();
});
// ...
https://www.npmjs.com/package/timezone-mock
I ended up getting around this by mocking the toLocaleString (or whatever toString method you are using) prototype. Using sinon I did:
var toLocaleString;
beforeAll(() => {
toLocaleString = sinon.stub(Date.prototype, 'toLocaleString', () => 'fake time')
})
afterAll(() => {
toLocaleString.restore()
})
This way if you are generating strings straight from a Date object, you're still OK.
2020 solution that works for me
beforeEach(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(Date.parse(FIXED_SYSTEM_TIME));
});
afterEach(() => {
jest.useRealTimers();
});
If you're using new Date() constructor instead of Date.now you can do like below:
const RealDate = Date;
beforeEach(() => {
// #ts-ignore
global.Date = class extends RealDate {
constructor() {
super();
return new RealDate("2016");
}
};
})
afterEach(() => {
global.Date = RealDate;
});
This issue is a must visit if you're here.
Adding TZ=UTC to my .env file solved the issue for me.
A simple fact can make it easy.
Just use :
new Date('some string').
This will always give an invalid date and no matter which machine, it will always be invalid date.
cheers.
Try passing a random date like new Date(1466424490000), wherever you call new Date()
Is there a conventional way to attempt a group of asserts to always be evaluated before failing the test?
Let's say my test assesses the presence of some names on a page:
var pageContent = 'dummy page content';
//.include(haystack, needle, [message])
//Asserts that haystack includes needle.
assert.include(pageContent, 'Alice');
assert.include(pageContent, 'Bob');
assert.include(pageContent, 'John');
Now, if Alice is missing, the test would fail with a single error:
>AssertionError: expected 'dummy page content' to contain 'Alice'
However I want to be notified that all three names are missing since in this case failing one condition does not prevent from evaluating others.
Rather than writing a wrapper method that aggregates the would-be output of these checks and throws a single error, I was hoping there would third-party library that "specializes" in this sort of thing or perhaps in-built functionality I'm overlooking.
I can offer two approaches.
The first one, mentioned by #Peter Lyons, relies on converting multiple assertions into a single assertion on multiple values. To keep the assertion error message useful, it's best to assert on the list of names:
var expected = ['Alice', 'Bob', 'John'];
var found = expected.filter(function(name) {
return pageContent.indexOf(name) >= 0;
}
// assuming Node's require('assert')
assert.deepEqual(found, expected);
// Example error message:
// AssertionError: ["Alice","Bob","John"] deepEqual ["Alice"]
The second approach uses "parameterised test". I'll assume you are using BDD-style for specifying test cases in my code.
describe('some page', function() {
for (var name in ['Alice', 'Bob', 'John'])
itContainsString(name);
function itContainsString(name) {
it('contains "' + name + '"', function() {
var pageContent = 'dummy page content';
assert.include(pageContent, 'Alice');
});
}
}
var found = ['Alice', 'Bob', 'John'].map(function (name) {
return pageContent.indexOf(name) >= 0;
});
assert.include(found, true);
If I may opine that your desire for a wrapper library for fuzzy asserting sounds misguided. Your fuzzy rules and heuristics about what is a "soft" vs "hard" assertion failure seem like a much less sensible alternative than good old programming using the existing assertion paradigm. It's testing. It's supposed to be straightforward and easy to reason about.
Keep in mind you can always take logic such as the above and wrap it in a function called includesOne(pageContent, needles) so it is conveniently reusable across tests.
Another approach to validating multiple assertions and getting feedback from all of them, regardless of which one fails first, is to use a Node.js module I wrote and published called multi-assert. Now, before we continue, I want to point out that, in your specific use case, if you're really only asserting 3 names, then I think Miroslav's answer is quite sufficient. As long as it serves your needs for quickly determining what broke and what to do to fix it, then you should continue on with that solution, and there's no need for additional dependencies in your project.
But the areas where I've run into major difficulties, or needed to spend more time debugging test failures or help others do the same, is when we've asserted properties of a really large object or array. For instance, when using deepEqual, I've personally run into cases where the error message can be quite complex, confusing, and unreadable in both HTML reports as well as the logs.
With the multi-assert module, the error messages are transparently displayed and specific to what we want to measure. For example, we can do something like this:
// import { multiAssert } from 'multi-assert';
const { assert } = require('chai');
const { multiAssert } = require('multi-assert');
describe('Page Content Tests', () => {
it('should contain Alice, Bob, and John somewhere in the content', () => {
var pageContent = 'dummy page content';
//.include(haystack, needle, [message])
//Asserts that haystack includes needle.
multiAssert([
() => assert.include(pageContent, 'Alice'),
() => assert.include(pageContent, 'Bob'),
() => assert.include(pageContent, 'John')
]);
});
});
And by running these tests, with "dummy page content", we're going to see the following error messages reported back to us with full transparency:
Page Content Tests
1) should contain Alice, Bob, and John somewhere in the content
0 passing (7ms)
1 failing
1) Page Content Tests
should contain Alice, Bob, and John somewhere in the content:
AssertionError:
MultipleAssertionError: expected 'dummy page content' to include 'Alice'
at /Users/user123/Dev/page-content-example/test/page-content.spec.js:13:20
at /Users/user123/Dev/page-content-example/node_modules/multi-assert/src/multi-assert.js:10:13
at Array.forEach (<anonymous>)
MultipleAssertionError: expected 'dummy page content' to include 'Bob'
at /Users/user123/Dev/page-content-example/test/page-content.spec.js:14:20
at /Users/user123/Dev/page-content-example/node_modules/multi-assert/src/multi-assert.js:10:13
at Array.forEach (<anonymous>)
MultipleAssertionError: expected 'dummy page content' to include 'John'
at /Users/user123/Dev/page-content-example/test/page-content.spec.js:15:20
at /Users/user123/Dev/page-content-example/node_modules/multi-assert/src/multi-assert.js:10:13
at Array.forEach (<anonymous>)
at multiAssert (node_modules/multi-assert/src/multi-assert.js:19:15)
at Context.<anonymous> (test/page-content.spec.js:12:5)
at processImmediate (node:internal/timers:466:21)
I want to also note that the multi-assert module also works with other testing frameworks; it's not just limited to Mocha and Chai; however, it's worth noting that Jasmine has been evaluating soft assertions by default for quite some time due to the nature of how their test runner and assertion library are more tightly integrated. If switching to Jasmine is not an easy or desired solution, and if existing assertion methods alone don't provide the desired level of feedback, then you can see the simplicity in wrapping up existing assertions in a multiAssert function call in order to achieve this transparency in your test cases. Hoping this helps!
The following is confusing me a lot. I have been spending quite a bit of time trying to understand why collection.find() doesn't work with regex passed as an object. The regex match is coming over HTTP wrapped in the body of a POST request. Then I try to gather the query (in string format) and perform the query. The problem seems to be that unless the regex is written inside Node without quotes, it won't work. That is, it must be a literal without quotes.
For example, the following works fine:
var query1 = {
company: {
'$regex': /goog/
}
};
collection.find(query1, {}).toArray(function (err, docs) {
// Got results back. Awesome.
});
However, if the data comes wrapped in an object, it doesn't return anything. I suspect it's because the value gets quoted behind the scenes (i.e. "/goog/"):
// Assume
var query2 = {
company: {
'$regex': query.company
}
};
collection.find(query2, {}).toArray(function (err, docs) {
// Got nothing back.
});
I have tested it with the mongo shell and I can confirm the following:
// Returns 5 results
db.getCollection("contacts").find( { "company": /goog/ } )
// Doesn't match anything
db.getCollection("contacts").find( { "company": "/goog/" } )
Furthermore, I just discovered the following: if I write the value with quotes
// Works fine
var companyRegex = {'$regex': /goog/};
var query3 = {
company: companyRegex
};
So technically, a "literal" regex without quotes wrapped in an object works fine. But if it's a string, it won't work. Even after trying to replace the double-quotes and single-quotes with nothing (i.e. essentially removing them.)
Any idea how can I get the regex match be passed verbatim to find()? I've researched it, finding lots of potential solutions, alas it's not working for me.
Thanks in advance!
Let me focus on one line of your post. This is where the problem might be:
The regex match is coming over HTTP wrapped in the body of a POST request.
This seems problematic because:
The only structures that survive serialization between client/server are:
boolean
number
string
null *
objects and arrays containing these basic types
objects and arrays containing object and arrays [of more obj/array] of these basic types
Regexp, Date, Function, and a host of others require reconstruction, which means
passing a string or pair of strings for the match and option components of the Regexp and running Regexp() on the receiving end to reconstruct.
Regexp gets a bit messy because Regexp.toString() and Regexp() do not appear to be inverses of each others: /someMatch/.toString() is "/someMatch/" but RegExp("/someMatch/") is //someMatch// and what was needed instead to rebuild the regexp was just RegExp("someMatch"), which is /someMatch/. I hope this helps.
JSON.stringify(/someMatch/) is {} (at least on Chrome).
So instead of trying to build a general transport, I recommend re instantiating a particular field as a regexp.
* Irrelevant note: (null is fine but undefined is peculiar. JSON won't stringify undefineds in objects and turns undefined into null in Arrays. I recognize this isn't part of your problem, just trying to be complete in describing what can be serialized.)