Server-side unit testing, does anyone else do it this way?

I inherited our current codebase with existing server-only unit tests that look nothing like the examples in the Meteor guide. Does anyone else roll their tests this way (see below for details)?

We have found these tests to be very effective and have not even considered investing in any full stack or client-side tests (maybe a happy flow some day). The tests include utility method testing from the /imports and /server directories but most importantly test our internal APIs (many meteor methods and a few publications).

Here’s the general pattern for a test. It connects the test code to the running instance, assigns a user id to the connection instead of logging in and invokes some methods that are supposed to return something or mutate the database in a way that can be inspected using other meteor methods or in some cases by reviewing mongodb collection contents directly.

import "some file that ensures that all method registrations are set up along with collection instances"

// Create a connection to the server and
// associates the connection with user with {{ userId }}.
// This enables "logged in" method calls.
export function withUser(userId: string) {
  if (!Meteor.isTest) {
    throw new Meteor.Error("Only available in unit tests");
  }
  const connection = DDP.connect(Meteor.absoluteUrl());
  connection.call("setUserId", userId);
  return connection;
}

describe("Top level thing", () => {
  let conn: DDP.DDPStatic;
  before(() => {
    resetDatabase(); // from "meteor/xolvio:cleaner"
    const userId = (code that creates a user, using the Accounts package)
    conn = withUser(userId);
  });

  after(() => {
    conn.disconnect();
  });

 it("should test something", () => {
   const result = conn.call("my_method", ...methodparameters);
   expect(result).to.be.something;
});

As I say, this pattern was not my invention but we use it with great success and would recommend it to anyone that builds apps in the same style we do (focusing on isolation between server and client by using well defined API methods, not implicit shared collections and whatnot).

1 Like

can you use this method to test publish/subscribe feature?
for method testing only, if you want to add userId and connection data, you can use .apply function:

      const theMethod = Meteor.server.method_handlers['some.method']
      const thisContext = {
        userId: null, // you can add specific user id here
      }
      let result
      expect(() => {
        result = theMethod.apply(thisContext)
      }).not.to.throw()
      // ...

Yes I use this for subscriptions as well - the DDPStatic class has a subscribe method that returns a SubscriptionHandle that you can interact with in the tests.

I obviously have some helpers, but here’s one subscription test example:

// helper that uses other helpers - I think you get the gist of it
export const subscribeForTest = <F extends (...args: any[]) => any>(
  conn: DDP.DDPStatic,
  sub: PubSubHelper<F>,
  ...args: Parameters<F>
): {
  collection: Mongo.Collection<DataType<typeof sub>>;
  handle: Meteor.SubscriptionHandle;
  ready: Promise<void>;
} => {
  const { promise: ready, resolver: onReady } = promiseWithResolver();
  const callbacks: SubscriptionCallbacks = { onReady };
  const handle = sub.subscribeWithConnection(conn, callbacks, ...args);
  const collection = sub.createCollection(conn);
  return { collection, handle, ready };
};


      const { collection, handle, ready } = subscribeForTest(
        connection,
        GlobalSearchPubSub,
        ...args according to the signature of the subscription - encoded in the GlobalSearchPubSub type here
      );
      await ready;
      expect(handle.ready()).to.be.true;
      const results  = collection.find({}).fetch();
// check that the results contain what they should
1 Like

The server-side implementation of setUserId is as follows:

// This condition is VERY important as we dont want to expose this
// functionality in production.
if (Meteor.isTest) {
  Meteor.methods({
    setUserId(userId) {
      this.setUserId(userId);
    },
  });
}