Meteor-Community-Packages / meteor-collection-hooks

Meteor Collection Hooks
https://atmospherejs.com/matb33/collection-hooks
MIT License
657 stars 92 forks source link

How to mock the database for testing? #202

Closed umbertooo closed 7 years ago

umbertooo commented 7 years ago

I'm working on a project that uses collection hooks a lot. Let's say there are two collections Widgets and Activities. Widgets has a after-insert-hook to insert a new Activity-document to log the fact that a user created a new widget.

In order to test this behavior I was planning to insert a widget and search the Activities collection for the expected document to be created. As the Meteor Guide recommends, I tried https://github.com/hwillson/meteor-stub-collections with no success.

It looks like StubCollections wants to create stubs for the same functions (insert/update/...) that CollectionHooks wrapped before. Thus the stubs are without effect.

How could this be done?

dmitrijs-balcers commented 7 years ago

I see two solutions:

  1. Use real DB, which is designated to be used for testing purposes. You can use it to perform integration tests.
  2. In case if you want to unit-test after hook, just test the callback that you are giving to after.insert in isolation.

I'm not sure if you really need to mock the database, so to say.

namirsab commented 7 years ago

as @dmitrijs-balcers says, you don't need to mock the complete database if you want to test a callback hook. collection-hooks works, so you don't need to test its behaviour. You need to test your function. Remember that when you are unit testing, you are testing your code, not others.

So rely on collection-hooks working well, and just test your hooks.

For instance, if you want to test and after or before hook, you just need to test that function inside. Imagine we are adding a field on before.insert:

const beforeInsertHook = function (userId, doc) {
    doc.createdBy = userId;

};

// The test you need to run is the following
describe('beforeInsertHook', function () {
    it('should add the createdAt field to the inserted document', function () {
       const insertedDoc = { _id: '1', label: 'myText' };
       beforeInsertHook('testUserId', doc);
       expect(doc.createdBy).to.equal('testUserId');
    });
});

If you need to use this inside your test, just call the function with call or apply:

describe('beforeInsertHook', function () {
    it('should add the createdAt field to the inserted document', function () {
       const insertedDoc = { _id: '1', label: 'myText' };
       const context = { transform: () => .... );
       beforeInsertHook.call(context, 'testUserId', doc);
       expect(doc.createdBy).to.equal('testUserId');
    });
});
dmitrijs-balcers commented 7 years ago

I wonder is it possible to refactor collection hook implementation so immutable data structures would be used. Otherwise mutating the attribute object is quite bad design I think.

It would really nice to see something like that:

const beforeInsertHook = function (userId, doc) {
    return doc.set("createdBy", userId); // doc.set would return a new object just like in immutable.js
};

In such way our callbacks would be pure functions, without side effects. Which makes it more predictible and easier to test.

namirsab commented 7 years ago

@dmitrijs-balcers I totally agree with you. But that would break the current API. Maybe you can write a PR on that. Or we can create a new major version on that. Or you can fork it and create your own version :smile:

umbertooo commented 7 years ago

I understand your point and agree with you that the callbacks should be tested in isolation, without hooks, without database. As @dmitrijs-balcers says, the database it not really required in this case. It just adds more complexity to the problem. And as the collection hooks are well tested, it is not necessary to retest it in my code.

SimonSimCity commented 6 years ago

The things you point out here are all valid, but I want to point out another thing. I currently am working on a project that uses todda00:collection-revisions, which realizes revisions based on this package.

Now, when testing my interface, I want to create some sample data by the package todda00:collection-revisions to be closest to the outcome I'll have as integration test.

I want to mock the database so I don't get errors because I blocked users of modifying the data outside of methods ...

I'll now just use the mocked database and call the hooks, registered by todda00:collection-revisions manually, but it might be of interest considering this.

If anyone else is looking for this, here's what I did. Feel free to modify it ...


  const update = (collection, sel, modifier, options) => {
    const doc = collection.findOne(sel);
    collection._hookAspects.update.before.forEach((a) => a.aspect(
      null,
      doc,
      CollectionHooks.getFields(modifier),
      modifier,
      options
    );

    collection.update(sel, modifier, options);
  }