nodejs-integration-tests-best-practices icon indicating copy to clipboard operation
nodejs-integration-tests-best-practices copied to clipboard

Recipe idea: Stateful factory as a good pattern

Open goldbergyoni opened this issue 5 years ago • 5 comments

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]

goldbergyoni avatar Dec 17 '20 08:12 goldbergyoni

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 avatar Dec 19 '20 12:12 mikicho

@mikicho

Whenever there is a big and nested object, a factory is the only way. There are multiple design dimensions to consider:

  1. Stateless vs stateful - The factory can generate the next sequence vs the test/factory uses some random to create unique data

  2. Home-made factory vs 3rd like Rosie. The later also dictates more boilerplate code to define the schema

  3. 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?

goldbergyoni avatar Dec 20 '20 15:12 goldbergyoni

It get only values you want.. (otherwise it will take the default values) And it creates the resources for the test.

mikicho avatar Dec 22 '20 07:12 mikicho

@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()}`;
};

goldbergyoni avatar Feb 05 '21 08:02 goldbergyoni

LGTM we can also use nano id instead

mikicho avatar Feb 05 '21 18:02 mikicho