Recipe idea: Stateful factory as a good pattern
It's too easy for integration test to step on each other toes by adding identical records when there is a unique constraint. For example, test 11 tries to add the user {name: 'messi'}, but also test 47 adds the same user. One of them will fail because 'name' is unique.
Solution 1, what I used by now - Each test add a timestamp:
const userToAdd = {name:messi ${Math.random()})
Solution 2, use a stateful factory - The test just calls some factory that manages the state and always provides fresh records:
dataFactory.getUser('messi')//returns 'messi-55', or any unique number
A better approach might be to use some factory lib like rosie: https://www.npmjs.com/package/rosie
Which one is better?
[Based on comments from @mikicho]
I prefer the factory approach. it's cleaner and remove irrelevant/repetitive LOCs from the test.
Also, I want to "call and raise" this idea. What I'd really like, is a declarative way to do this to include sub resources, for example:
createMeeting({
name: '2020 Summary',
startsAt: new Date(),
topics: [
{ }, // create topic with default values
{
name: 'topic 2',
items: 2 // create 2 items with default values inside topic 2
}
]
});
Now, I'm not sure if it's worth the effort. but our sub-resources starts with:
it ('should...', () => {
// Arrange
const meeting = await createMeeting();
const [topic1, topic 2] = Promise.all([createTopic({ meetingId: meeting,id }), createTopic({ meetingId: meeting,id, name: 'topic2' })]);
const items = Promise.all([createItem({ topicId: topic1.id }), createItem({ topicId: topic2.id })]);
});
...
And so on.
Sometimes we create some file-scoped main resources (like meeting and topics) and use them in all tests but not sure if this the best approach.
WDYT?
p.s: I thought about this "declarative API" (😅) while writing this comment so don't stick to this specific implementation.. but the idea in general.
@mikicho
Whenever there is a big and nested object, a factory is the only way. There are multiple design dimensions to consider:
-
Stateless vs stateful - The factory can generate the next sequence vs the test/factory uses some random to create unique data
-
Home-made factory vs 3rd like Rosie. The later also dictates more boilerplate code to define the schema
-
Test is aware of the structure vs agnostic - Should we need to craft a meeting, our factory may fully abstract the structure of it:
testHelper.factorMetting(howManyTopics, meetingValuesOverrides: {name: 'Kickoff'}) // Using this style, the tests are really short and not bogged down with the objects schema.
That said, their flexibility to alter the structure is diminished, so you'll more often see stuff like:
theFactoredMeeting.topics.push(...)
Another style is what you exemplified where the tests are very involved in the object graph and might get quite long
In your example, what createMeeting function does given that it already gets almost the entire object?
It get only values you want.. (otherwise it will take the default values) And it creates the resources for the test.
@mikicho @DanielGluskin As I experimented with this, I believe that random is the only way to go because it is unique across files, processes and time (unlike rosie increment). WDYT?
module.exports.getShortUnique = () => {
const now = new Date();
// We add this weak random just to cover the case where two test started at the very same millisecond
const aBitOfMoreSalt = Math.ceil(Math.random() * 99);
return `${process.pid}${aBitOfMoreSalt}${now.getMilliseconds()}`;
};
LGTM we can also use nano id instead