Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I unit test my meteor methods?

I've written some Meteor methods for my application containing various chunks of business logic. Now I'd like to write unit tests for those methods. By unit tests, I specifically mean fast tests which do not:

  • Perform XHRs or
  • Write to the database

How can I go about doing this? My current thinking is that when I start up the Meteor server in a test configuration I should replace my collections with dummy collections (by passing new Meteor.Collection(null)) and have my unit tests run on the server side, invoking Meteor.call() on each of my methods in turn from there. I'm not entirely sure how I'd kick off the tests, possibly I'd want to build a custom /tests URL into my application which fires them off. Does this approach seem reasonable? Are there any libraries/packages out there that would make writing unit tests for my methods easier?

like image 945
Derek Thurn Avatar asked Mar 03 '13 21:03

Derek Thurn


People also ask

How do you run a unit test?

To run all the tests in a default group, choose the Run icon and then choose the group on the menu. Select the individual tests that you want to run, open the right-click menu for a selected test and then choose Run Selected Tests (or press Ctrl + R, T).

What is unit test assert?

The assert section ensures that the code behaves as expected. Assertions replace us humans in checking that the software does what it should. They express requirements that the unit under test is expected to meet. Now, often one can write slightly different assertions to capture a given requirement.

What are unit tests Swift?

A unit test is a function you write that tests something about your app. A good unit test is small. It tests just one thing in isolation. For example, if your app adds up the total amount of time your user spent doing something, you might write a test to check if this total is correct.


1 Answers

All right, here's what I came up with to unit test my methods. I'll be the first to admit there's a lot of room for improvement in this!

First, in my server.coffee file I have the following code:

Meteor.startup ->
  return unless Meteor.settings["test"]
  require = __meteor_bootstrap__.require
  require("coffee-script")
  fs = require("fs")
  path = require("path")
  Mocha = require("mocha")

  mocha = new Mocha()
  files = fs.readdirSync("tests")
  basePath = fs.realpathSync("tests")
  for file in files
    continue unless file.match(/\.coffee$/) or file.match(/\.js$/)
    continue if file[0] == "."
    filePath = path.join(basePath, file)
    continue unless fs.statSync(filePath).isFile()
    mocha.addFile(filePath)
  mocha.run()

First of all this code only runs if Meteor.settings["test"] has been defined, which I can do when I run my tests locally, but which should never be true in production. It then searches the "tests" directory for javascript or coffeescript files (subdirectories aren't searched in my implementation, but it would be easy to add that) and adds them to a mocha instance. I'm using the excellent mocha javascript testing library here, combined with the chai assertion library.

All of this code is wrapped inside a Meteor.startup call so that my unit tests run on server start. This is especially nice because Meteor automatically re-runs my tests whenever I change any of my code. Because of the decision to isolate out the database and not perform XHRs, my tests run in a few milliseconds, so this isn't very annoying.

For the tests themselves, I need to do

chai = require("chai")
should = chai.should()

To pull in the assertion library. There are still a couple tricky problems to be solved, though. First of all, Meteor method calls will fail if they're not wrapped in a Fiber. I don't currently have a very good solution to this problem, but I created the itShould function to replace mocha's it function and wrap the test body inside a Fiber:

# A version of mocha's "it" function which wraps the test body in a Fiber.
itShould = (desc, fn) ->
  it(("should " + desc), (done) -> (Fiber ->
    fn()
    done()).run())

Next up is the problem of, for testing purposes, replacing my collections with mock collections. This is very difficult to do if you follow the standard Meteor practice of putting your collections in global variables. However, if you make your collections properties on a global object, you can do it. Simply make your collections via myApp.Collection = new Meteor.Collection("name"). Then, in your tests, you can have a before function mock out the collection:

realCollection = null
before ->
  realCollection = myApp.Collection
  myApp.Collection = new Meteor.Collection(null)
after ->
  myApp.Collection = realCollection

This way, your collection is mocked out for the duration of the test run, but then it's restored so you can interact with your app normally. Some other things are possible to mock via a similar technique. For example, the global Meteor.userId() function only works for client-initiated requests. I've actually filed a bug against Meteor to see if they can provide a better solution to this problem, but for now I'm just replacing the function with my own version for testing:

realUserIdFn = null
before ->
  realUserIdFn = Meteor.userId
  Meteor.userId = -> "123456"
after ->
  Meteor.userId = realUserIdFn

This approach works for some parts of Meteor, but not all of it. For example, I haven't found a way to test methods that invoke this.setUserId yet, because I don't think there's a good way to mock out that behavior. On the whole, though, this approach is working out for me... I love being able to re-run my tests automatically when I change the code, and running tests in isolation is just generally a good idea. It's also very convenient that tests on the server can block, making them simpler to write without callback chains. Here's what a test would look like:

describe "the newWidget method", ->
  itShould "make a new widget in the Widgets collection", ->
    widgetId = Meteor.call("newWidget", {awesome: true})
    widget = myApp.Widgets.findOne(widgetId)
    widget.awesome.should.be.true
like image 129
Derek Thurn Avatar answered Nov 09 '22 05:11

Derek Thurn