Code Monkey home page Code Monkey logo

respec-given's Introduction

respec-given

rspec-given in JavaScript, on top of mocha.

  • encourages cleaner, readable, and maintainable tests using Given/When/Then API
  • test async code without boilerplate code. support callback/Promise/generator/Observable/async function
  • descriptive assertion message

Demo

usage demo

Installation

install respec-given locally

npm install --save-dev respec-given

Usage

Node.js

with mocha command:

mocha --ui respec-given --require respec-given/na-loader

see more about natural assertion loaders

Browser

add script tag after load mocha:

<script src="node_modules/respec-given/mocha/browser.js"></script>
<script>mocha.setup('respec-given')</script>

natural assertion transformation tools is not available yet, but you won't wait too long :)

Example

Here is a spec written in respec-given.

const Stack = require('../stack');

describe('Stack', () => {

  const stack_with = (initial_contents) => {
    const stack = Object.create(Stack);
    initial_contents.forEach(item => stack.push(item))
    return stack;
  };

  Given('stack', $ => stack_with($.initial_contents))
  Invariant($ => $.stack.empty() === ($.stack.depth() === 0))

  context("with no items", () => {
    Given('initial_contents', () => [])
    Then($ => $.stack.depth() === 0)

    context("when pushing", => {
      When($ => $.stack.push('an_item'))

      Then($ => $.stack.depth() === 1)
      Then($ => $.stack.top() === 'an_item')
    })
  })

  context("with one item", () => {
    Given('initial_contents', () => ['an_item'])

    context("when popping", () => {
      When('pop_result', $ => $.stack.pop())

      Then($ => $.pop_result === 'an_item')
      Then($ => $.stack.depth() === 0)
    })
  })

  context("with several items", () => {
    Given('initial_contents', () => ['second_item', 'top_item'])
    GivenI('original_depth', $ => $.stack.depth())

    context("when pushing", () => {
      When($ => $.stack.push('new_item'))

      Then($ => $.stack.top() === 'new_item')
      Then($ => $.stack.depth() === $.original_depth + 1)
    })

    context("when popping", () => {
      When('pop_result', $ => $.stack.pop())

      Then($ => $.pop_result === 'top_item')
      Then($ => $.stack.top() === 'second_item')
      Then($ => $.stack.depth() === $.original_depth - 1)
    })
  })

Before we take a closer look at each statement used in respec-given, I hope you can read rspec-given's documentation first. It explained the idea behind its design excellently.

Instead of repeat Jim's words, I'll simply introduce API here.

Given

Given is used to setup precondition. There are 2 types of Given: lazy Given and immediate Given.

Lazy Given

    // Given("varname", fn)
    Given('stack', () => stack_with([]))

$.stack become accessible in other clauses.

The function will be evaluated until first access to $.stack. Once the function is evaluated, the result is cached.

If you have multiple Givens, like

    Given('stack1', () => stack_with([1, 2]))
    Given('stack2', () => stack_with([3, 4]))

you can use object notation as a shorthand:

    // Given({hash})
    Given({
      stack1: () => stack_with([1, 2]),
      stack2: () => stack_with([3, 4])
    })

Immediate Given

There are several form of immediate Given. The first category is Given(fn). Since it doesn't specify a variable name, it is for side-effects. The Second category is GivenI(varname, fn). It will evaluate fn immediately and assign the returned result to this.varname. If error occurs in fn, GivenI expression will fail.

    // Given(fn)
    Given($ => stack_with([]))

Using this form, the function will be evaluated immediately.

    Given(() => new Promise(...))

If the function returns a Promise, next statement will be executed until the promise is resolved.

    Given(() => new Observable(...))

If the function returns an Observable, next statement will be executed until the observable is complete.

    // Given(fn(done))
    Given(($, done) => {
      asyncOp(done)
    })

Using this form, you can perform an asynchronous operation. When finished, you should call done() is success, or done(err) if the operation failed.

    // Given(generatorFn)
    Given(function*() { yield yieldable })

generator function is also supported. It will be executed until it returns or throws.

    // GivenI("varname", fn)
    GivenI('stack', () => stack_with([]))

Using this form, the function will be evaluated immediately and the return value is assigned to $.stack.

Note unlike lazy-Given, if fn returns a Promise or an Observable, it will be resolved/completed automatically.

Also, fn can have a callback with signature (err, res), so you can perform asynchronous operation.

fn can also be a generator function. the returned value will be assigned to $.varname.

Let statement

rspec has let helper, which is a lazy variable declaration. In rspec, Given is simply alias to let. (Jim mentioned a reason why we need let if we have Given already.)

Since ES6 introduce let keyword, to avoid name collision, respec-given choose capitalized Let.

  • Let is an alias to Given, which maps to rspec-given's let/Given
  • LetI is an alias to GivenI, which maps to rspec-given's let!/Given!

When

When is used to perform action and capture result (or Error). All asynchronous operation should be performed here.

    // When(fn)
    When($ => $.stack.pop())

this function will be executed immediately.

    When(() => new Promise(...))

If the function returns a Promise, next statement will be executed until the promise is resolved.

    When(() => new Observable(...))

If the function returns an Observable, next statement will be executed until the observable is complete.

    // When(fn($, done))
    When(($, done) => {
      asyncOp(function(err, res) {
        done(err, res)
      })
    })

Using this form, you can perform an asynchronous operation. When finished, you should call done() is success, or done(err) if the operation failed.

    // When(generatorFn)
    When(function*() { yield yieldable })

generator function is also supported. It will be executed until it returns or throws.

    // When("result", fn)
    When('pop_result', $ => $.stack.pop())
    Then($ => $.pop_result === 'top_item')

Using this form, the function will be executed immediately and the return value is assigned to $.pop_result.

    When('result1', () => Promise.resolve(1))
    When('result2', () => Promise.reject(2))
    Then($ => $.result1 === 1)
    Then($ => $.result2 === 2)

If the function return a Promise, the promise will be resolved to a value (or an error) first, then assign the resolved value to $.result.

    When('result', () => throw new Error('oops!'))
    Then($ => $.result.message === 'oops')

If the function throws an error synchronously, the error will be caught and assigned to $.result.

    // When("result", fn($, done))
    When('result', ($, done) => {
      asyncOp(function(err, res) {
        done(err, res)
      })
    })

Using this form, you can perform asynchronous operation here. If operation succeed, you should call done(null, res), and res will be assigned to $.result. If operation failed, you should call done(err), and err will be assigned to $.result.

If the function throws an error synchronously, the error will be caught and assigned to $.result.

    // When("result", generatorFn)
    When('result', function*() { return yield yieldable })

generator function is also supported. It will be executed until it returns or throws. The value it returns or throws will be assigned to $.result.

If you have multiple Whens, like

    When('result1', () => stack1.pop())
    When('result2', () => stack2.pop())

you can use object notation as a shorthand:

    // When({hash})
    When({
      result1: () => stack1.pop()),
      result2: () => stack2.pop())
    })

Then

A Then clause forms a mocha test case of a test suite, it is like it in classical BDD style mocha test. Note that:

  1. Then should only contain an assertion expression
  2. Then should not have any side effects.
  3. Then only support synchronous operation. all asynchronous operation should be done in When clause.

Let me quote Jim's words here:

Let me repeat that: Then clauses should not have any side effects! Then clauses with side effects are erroneous. Then clauses need to be idempotent, so that running them once, twice, a hundred times, or never does not change the state of the program. (The same is true of And and Invariant clauses).

OK, let's see some example!

    // Then(fn)
    Then($ => expect($.result).to.be(1))

This form uses a 3rd-party assertion/matcher library, for example, chai.js.

    Then($ => $.result === 1)

This form returns a boolean expression, this is called natural assertion. if the function returns a boolean false, this test is considered fail.

And

please refer to rspec-given's documentation

Invariant

please refer to rspec-given's documentation

Execution Ordering

please refer to rspec-given's documentation

Natural Assertions

respec-given supports "natural assertions" in Then, And, and Invariant blocks. Natural assertions are just boolean expressions, without additional assertion library.

Failure Messages with Natural Assertions

There are 2 kind of failure message, depends on whether test code is transformed.

If the test code is not transformed, simple failure message applies. Otherwise, comprehensive failure message applies. The former simply points out which expression failed, the later show each subexpression's value, which is easier for developers to debug.

Simple Failure Message

example:

     Error: Then { $.stack.depth() === 0 }

       Invariant expression failed.
       Failing expression: Invariant { $.stack.empty() === ($.stack.depth() === 0) }

Comprehensive Failure Message

example:

  Error: Then { $.stack.depth() === 0 }

  Invariant expression failed at test/stack_spec.coffee:23:13

         Invariant { $.stack.empty() === ($.stack.depth() === 0) }
                       |     |    |  |      |     |    |  |
                       |     |    |  |      |     |    0  true
                       |     |    |  |      |     #function#
                       |     |    |  false  Object{_arr:[]}
                       |     |    false
                       |     #function#
                       Object{_arr:[]}

         expected: false
         to equal: true

Checking for Errors with Natural Assertions

If you wish to see if the result of a When clause is an Error, you can use the following:

When('result', () => badAction())
Then($ => Failure(CustomError, /message/).matches($.result))
Then($ => Failure(CustomError).matches($.result))
Then($ => Failure(/message/).matches($.result))

Transform test code

What is a natural assertion loader?

Natural assertion loader is a tool which analysis test code's Then expression, gather context information, and generate code that carries these information. When assertion failed (return false), these information are used to evaluate failed Then clause's subexpression and generate diagnosis message for you.

Why tooling?

Because in JavaScript, lexical binding can not be "captured" during execution time. Lexical binding is resolved at lex time, it's the world view of specific block of code. You have no way to share this view to others (in JavaScript). For example:

    const x = 1
    Then(() => x === 0)

Then received a function, which returns false. Even Then can know x's existence by analysis fn.toString(), Then have no way to access x. No.

This is a meta-programming problem, which can not be solved in JavaScript itself. That's why we need a loader (preprocessor, transpiler, instrumenter, whatever you like to call it).

When do I need it?

When you use natural assertion, transformed test code would generate more helpful error message for you.

On the other hand, if you are using assertion library (like node.js built-in assert, chai.js, expect.js, or shouldjs), which provide their diagnosis message already, then you don't need natural assertion loader.

Ok. Tell me how to use it.

there are 3 Node.js loader out of the box:

  • JavaScript loader

    mocha --ui respec-given --require respec-given/na-loader
  • CoffeeScript loader

    mocha --ui respec-given --require respec-given/na-loader/coffee
  • LiveScript loader

    mocha --ui respec-given --require respec-given/na-loader/ls

Acknowledgments

respec-given's People

Contributors

cades avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

kytu800 fossabot

respec-given's Issues

Errors due to Mocha versioning

If I npm install both respec-given and Mocha, I always get "Uncaught error outside test suite". I believe this is because NPM puts Respec-Given's Mocha dependency inside Respec-Given's node_modules folder in order to put the manually installed Mocha in the main node_modules folder, so the Mocha that Node finds when Respec-Given requires it and the Mocha that is used to run through node_modules/.bin/mocha are actually two different copies.

It may be more appropriate to list Mocha as a peer dependency rather than a regular dependency, since peer dependencies are intended for packages that are addons or plugins for their peer dependency and since it will require the user to separately have installed a compatible Mocha version rather than allowing them to separately install an incompatible version that creates this scenario.

Should lazy Given variables be evaluated before override?

Currently, this test will pass:

Given x: -> throw new Error 'oops!'
When -> @x = 'override x'
Then -> "it is ok"

but in rspec-given, this will fail.

Javascript provide Object.defineProperties, which allow developers to define getter and setter separately. As a result, execute setter without executing getter is possible. The question here is: which behavior should it have? Does the semantic is consistent with general evaluation rule?

At this moment, I have no idea about this choice. Have to survey how other projects dealing with this issue.

Any suggestion is welcome!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.