Adds a Given-When-Then DSL to jasmine as an alternative style for specs
We just released a version 2.0.0, thanks to the contributions of @ronen, to bring jasmine-given closer to parity with rspec-given. In particular, jasmine-given will now:
Givenstatements will always execute before any
Whenstatements. This is counter-intuitive at first, but can really help you DRY up specs that require variable setup.
Andin place of multiple
Thenstatements; when using
Then, the set-up will only be executed for the first
Then, which could be a significant speed-up depending on the test while looking cleaner than chaining
Thenstatements with parentheses.
Keep in mind that the former will be a breaking change for many test suites that currently use jasmine-given, so be sure to allot yourself some time to address any test failures that occur because a
Given was incidentally placed after a
When in a way that doesn't agree with the new execution order.
The basic idea behind the "*-given" meme is a humble acknowledgement of given-when-then as the best English language analogue we have to arrange-act-assert. With rspec and jasmine, we often approximate "given-when-then" with "let-beforeEach-it" (noting that jasmine lacks
The big idea is "why approximate given-when-then, when we could actually just use them?"
The small idea is "if we couldn't write English along with our
it blocks then we'd be encouraged to write cleaner, clearer matchers to articulate our expectations."
The subtle idea is that all "given"s should be evaluated before the "when"s. This can DRY up your specs: you don't need to repeat a series of "when"s in order to test the final result with different initial "given"s.
All ideas are pretty cool. Thanks, Jim!
Oh, and jasmine-given looks much nicer in CoffeeScript, so I'll show that example first:
describe "assigning stuff to this"->Given -> @number = 24Given -> @number++When -> @number *= 2Then -> @number == 50# orThen -> expect@numbertoBe50describe "assigning stuff to variables"->subject=nullGiven -> subject =When -> subjectpush'foo'Then -> subjectlength == 1# orThen -> expectsubjectlengthtoBe1
As you might infer from the above,
Then will trigger a spec failure when the function passed to it returns
false. As shown above, traditional expectations can still be used, but using simple booleans can make for significantly easier-to-read expectations when you're asserting something as obvious as equality.
The execution order for executing a
Then is to execute all preceding
from the outside in, and next all the preceding
When blocks from the outside in, and
Then. This means that a later
Given can affect an earlier
While this may seem odd at first glance, it can DRY up your specs, especially if
you are testing a series of
When steps whose final outcome depends on an
initial condition. For example:
Given -> userWhen -> login userdescribe "clicking create", ->When -> createButton.click()Then -> expect(ajax).toHaveBeenCalled()describe "creation succeeds", ->When -> ajax.success()Then -> object_is_shown()describe "reports success message", ->Then -> feedback_message.hasContents "created"describe "novice gets congratulations message", ->Given -> user.isNovice = trueThen -> feedback_message.hasContents "congratulations!"describe "expert gets no feedback", ->Given -> user.isExpert = trueThen -> feedback_message.isEmpty()
For the final three
Thens, the execution order is:
Given -> userWhen -> login userWhen -> createButton.click()When -> ajax.success()Then -> feedback_message.hasContents "created"Given -> userGiven -> user.isNovice = trueWhen -> login userWhen -> createButton.click()When -> ajax.success()Then -> feedback_message.hasContents "congratulations!"Given -> userGiven -> user.isExpert = trueWhen -> login userWhen -> createButton.click()When -> ajax.success()Then -> feedback_message.isEmpty()
When execution order, the only straightforward way to get the above
behavior would be to duplicate then
Whens for each user case.
Jim mentioned to me that
Then blocks ought to be idempotent (that is, since they're assertions they should not have any affect on the state of the subject being specified). As a result, one improvement he made to rspec-given 2.x was the
And method, which—by following a
Then—would be like invoked n
Then expectations without executing each
When blocks n times.
Take this example from jasmine-given's spec:
describe "eliminating redundant test execution"->describe "a traditional spec with numerous Then statements"->timesGivenWasInvoked = timesWhenWasInvoked = 0Given -> timesGivenWasInvoked++When -> timesWhenWasInvoked++Then -> timesGivenWasInvoked == 1Then -> timesWhenWasInvoked == 2Then -> timesGivenWasInvoked == 3Then -> timesWhenWasInvoked == 4
Because there are four
Then statements, the
When are each executed four times. That's because it would be unreasonable for Jasmine to expect each
it function to be idempotent.
However, spec authors can leverage idempotence safely when writing in a given-when-then format. You opt-in with jasmine-given by using
And blocks, as shown below:
describe "chaining Then statements"->timesGivenWasInvoked = timesWhenWasInvoked = 0Given -> timesGivenWasInvoked++When -> timesWhenWasInvoked++Then -> timesGivenWasInvoked == 1And -> timesWhenWasInvoked == 1And -> timesGivenWasInvoked == 1And -> timesWhenWasInvoked == 1Then -> timesWhenWasInvoked == 2
In this example,
When are only invoked one time each for the first
Then, because jasmine-given rolled all of those
And statements up into a single
it in Jasmine. Note that the label of the
it is taken from the
Leveraging this feature is likely to have the effect of speeding up your specs, especially if your specs are otherwise slow (integration specs or DOM-heavy).
Rspec-given also introduced the notion of "Invariants". An
Invariant lets you specify a condition which should always be true within the current scope. For example:
Given -> @stack = new MyStack @initialContentsInvariant -> @stack.empty? == (@stack.depth == 0)describe "With some initial contents", ->Given -> @initialContents = ["a", "b", "c"]Then -> @stack.depth == 3describe "Pop one", ->When -> @result = @stack.popThen -> @stack.depth == 2describe "Clear all", ->When -> @stack.clear()Then -> @stack.depth == 0describe "With no contents", ->Then -> @stack.depth == 0…etc…
Invariant will be checked before each
Then block. Note that invariants do not appear as their own tests; if an invariant fails it will be reported as a failure within the
Then block. Effectively, an
Invariant defines an implicit
And which gets prepended to each
Then within the current scope. Thus the above example is a DRY version of:
Given -> @stack = new MyStack @initialContentsdescribe "With some initial contents", ->Given -> @initialContents = ["a", "b", "c"]Then -> @stack.depth == 3And -> @stack.empty? == falsedescribe "Pop one", ->When -> @result = @stack.popThen -> @stack.depth == 2And -> @stack.empty? == falsedescribe "Clear all", ->When -> @stack.clear()Then -> @stack.depth == 0And -> @stack.empty? == truedescribe "With no contents", ->Then -> @stack.depth == 0And -> @stack.empty? == true…etc…
except that the
Invariant is tested before each
Then rather than after.
Jasmine-given labels your underlying
it blocks with the source expression itself, encouraging writing cleaner, clearer matchers -- and more DRY than saying the same thing twice, once in code and once in English. But there are times when we're using third-party libraries or matchers that just don't read cleanly as English, even when they're expressing a simple concept.
Or, perhaps you are using a collection of
And statements to express a single specification. So, when needed, you may use a label for your
Then "makes AJAX POST request to create item", -> expect(@ajax_spy).toHaveBeenCalled() And -> @ajax_spy.mostRecentCall.args.type = 'POST' And -> @ajax_spy.mostRecentCall.args.url == "/items" And -> @ajax_spy.mostRecentCall.args.data.item.user_id == userID And -> @ajax_spy.mostRecentCall.args.data.item.name == itemName
Following Jasmine 2.0's style for testing asynchronous code, the
When statements' functions can take a
done parameter, which is a function to call when the asynchronous code completes. Subsequent statements won't be executed until the
done completes. E.g.
Given (done) -> $.get "/stuff" .success (data) => @stuff = data done() When (done) -> $.post "/do", stuff: @stuff .success (data) => @yay = true done() Then -> @stuff == "the stuff" Then -> @yay
And statement functions can also take a
done parameter, if the expectation itself requires asynchronous executation to evalute. For example if you're using Selenium, you might want to check browser state in an expectation:
Then (done) -> browser.find '.alert', (el) -> expect(el).toBeDefined() done() And (done) -> browser.find '.cancel', (el) -> expect(cancel).toBeDefined() done()
To use this helper with Jasmine under Node.js, simply add it to your package.json with
$ npm install jasmine-given --save-dev
And then from your spec (or in a spec helper),
sure that it's loaded after jasmine itself is added to the
global object, or else
it will load
minijasminenode which will, in turn, load jasmine
global for you (which you may not be intending).