好吧,这就是我想出的对我的方法进行单元测试的方法。我将是第一个承认这方面有很大改进空间的人!
首先,在我的server.coffee
文件中,我有以下代码:
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()
首先,此代码仅在 Meteor.settings["test"] 已定义的情况下运行,当我在本地运行测试时我可以这样做,但在生产中永远不应该这样做。然后它在“tests”目录中搜索javascript或coffeescript文件(在我的实现中没有搜索子目录,但添加它很容易)并将它们添加到mocha
实例中。我在这里使用了优秀的mocha javascript 测试库,并结合了chai断言库。
所有这些代码都包含在一个Meteor.startup
调用中,以便我的单元测试在服务器启动时运行。这特别好,因为每当我更改任何代码时,Meteor 都会自动重新运行我的测试。由于决定隔离数据库而不执行 XHR,我的测试在几毫秒内运行,所以这不是很烦人。
对于测试本身,我需要做
chai = require("chai")
should = chai.should()
拉入断言库。不过,仍有一些棘手的问题需要解决。首先,如果 Meteor 方法调用没有封装在 Fiber 中,它们将失败。我目前对这个问题没有很好的解决方案,但是我创建了一个itShould
函数来替换 mocha 的it
函数并将测试体包装在 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())
接下来是为了测试目的,用模拟集合替换我的集合的问题。如果您遵循将集合放入全局变量的标准 Meteor 实践,这将非常困难。但是,如果您在全局对象上创建集合属性,则可以这样做。只需通过myApp.Collection = new Meteor.Collection("name")
. 然后,在您的测试中,您可以使用一个before
函数来模拟集合:
realCollection = null
before ->
realCollection = myApp.Collection
myApp.Collection = new Meteor.Collection(null)
after ->
myApp.Collection = realCollection
这样,您的集合在测试运行期间被模拟出来,但随后它会被恢复,以便您可以正常与您的应用程序交互。其他一些东西可以通过类似的技术来模拟。例如,全局Meteor.userId()
函数仅适用于客户端发起的请求。我实际上已经针对 Meteor 提交了一个错误,看看他们是否可以为这个问题提供更好的解决方案,但现在我只是用我自己的版本替换该函数进行测试:
realUserIdFn = null
before ->
realUserIdFn = Meteor.userId
Meteor.userId = -> "123456"
after ->
Meteor.userId = realUserIdFn
这种方法适用于 Meteor 的某些部分,但不是全部。例如,我还没有找到一种方法来测试调用的方法this.setUserId
,因为我认为没有很好的方法来模拟这种行为。不过,总的来说,这种方法对我很有效……我喜欢在更改代码时能够自动重新运行我的测试,并且单独运行测试通常是一个好主意。服务器上的测试可以阻塞也非常方便,使得它们在没有回调链的情况下更容易编写。这是测试的样子:
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