Skip to content
/ tea Public

a lightweight Test Environment Application ("tea")

Notifications You must be signed in to change notification settings

sc0ttj/tea

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tea logo

Tea

"Test environment application"

npm version Dependency Status devDependencies Status Node version Build Status npm version npm version

Tea is a software testing suite, similar to node-tap, tape, mocha, jasmine, etc, but much smaller, more lightweight, and with fewer features.

Features

  • fast, lightweight, minimal code (~4.3kb minified and gzipped)
  • very little setup, a fairly complete solution
  • includes assertions, test harness, test runner, test reporter
  • all in a single file, src/tea.js
  • supports flexible syntax for writing your tests:
    • multiple assertion methods and syntaxes
    • grouped and nested tests
  • report results in various formats (console, DevTools, TAP, debug)
  • works everywhere:
    • Browsers - show test results in the DevTools console
    • NodeJS - show test results in the terminal
    • CI environments - show results in the terminal
  • supports the following CLI options:
    • --quiet: only show failing tests
    • --fail-fast: exit after the first failing test
    • --verbose: show expected/actual for all tests (including passing tests)
    • --format=console|tap|debug: the format of the test results
    • --no-indent: don't indent grouped results (useful if passing test results to a TAP-format prettifier)

Installation

Install tea as a development dependency:

npm install @scottjarvis/tea --save-dev

Setup

Create a folder for your test files:

mkdir ./tests

Create a file to contain your tests:

touch ./tests/mytests.js

Usage

Initialise tea in tests/mytests.js:

tea = require('tea');

tea()  /* initialise tea by running it */

// add test stuff here

Adding tests

tea has various built-in assertion methods, and you should use them. But tests can be as simple as an expression.

Lets add some tests, so you have this in tests/mytests.js:

tea = require('tea');

tea()  /* initialise tea by running it */

/*   Optionally define some test hooks..
 test.beforeEach = () => console.log("before each")
 test.beforeAll  = () => console.log("before all")
 test.afterEach  = () => console.log("after each")
 test.afterAll   = () => console.log("after all")
 */

// tests without assertions or expected parameter (not recommended for TAP output)
test("simplest test", 1 === 1)
test("simplest test fails", 1 === 2)

// tests without assertions, with an 'expected' parameter
test("simple test, 1 + 1 should be '2'", 1+1, 2)
test("simple test fails, 1 + 1 should be '2'", 1+1, 3)

// a test using 'expect' assertion method
test("test using 'expect', description here", () => {
  expect("1 + 1 should equal 2 is true", 1 + 1 === 2, true)
  expect("1 + 1 should equal 2", 1 + 1, 2)
  expect("app should be undefined", typeof app, "undefined")
})

// run the tests
run()

Running your tests

Now you're ready to run your tests:

node tests/mytests.js

You should see (something like) this:

test results

Advanced usage

Usage:  node path/to/tests.js [options]

Options:

  --quiet            Only list failing tests
  --fail-fast        Exit after first failing test
  --verbose          List the actual/expected results of passing tests too
  --format=<name>    The style/format of the test results (console|debug|tap)
  --no-indent        Don't indent grouped test results (useful if piping to TAP prettifiers)
  --help             Show this help screen

For continuous integration (CI) environments, you might want to enable --quiet --fail-fast.

TAP output

To show test results in TAP format, use --format=tap on the command-line (or tea.reportFormat = 'tap' if in the browser/DevTools).

The TAP format is machine-readable, and you can pipe the results to other programs, to prettify it.

The TAP output of tea is already indented and colourised a bit, to make it easier to read.

But these TAP prettifiers work OK with the TAP output of tea:

Example (using tap-nyan)

NOTE: If piping to a TAP prettifier, it's recommended to use the --no-indent (or tea.noIndent = true) setting.

Using assertions

You can of course use assertions, instead of simple expressions.

The following assertions are built into tea:

  • assert
  • expect
  • should (alias of expect)
  • t

Using expect

Usage:

expect(msg, actual, expected)

Examples:

/* tests using the 'expect' assertion method */

test("test using expect, description here", () => {
  expect("1 + 1 should equal 2 is true", app.add(1, 1) === 2, true)
  expect("1 + 1 should equal 2", app.add(1, 1), 2)
  expect("app.c should be undefined", typeof app.c, "undefined")
})

// run the tests
run()

Using assert

Usage:

assert.foo(msg, actual, expected)

Where foo can be any of these methods:

  • assert.truthy() - checks if something is truthy
  • assert.falsey() - checks if something is falsey
  • assert.eq() - uses the == comparison (alias of assert.equals())
  • assert.strictEquals() - uses the === comparison
  • assert.deepEquals() - uses the react-fast-compare deep equals comparison
  • assert.isType() - uses a typeof comparison
  • assert.throwsError() - checks if something throws an Error
  • assert.isImmutable() - checks if something is a Boolean, Number, String, Null
  • assert.isMutable() - checks if something is an Object, Array, Function, Class, Map, or Set
  • assert.isReactElement() - checks object contains $$typeof = REACT_ELEMENT_TYPE

Examples:

/* tests using the 'assert' assertion method */

test("test using assert, description here", () => {
  assert.isType("app.add should be a function", app.add, "function")
  assert.isType("[1,2,3] should be an array", [1,2,3], "array")
  assert.equals("1 should equal 1", 1, 1)
  assert.strictEquals("1 + 1 should equal 2", app.add(1, 1), 2)
  assert.deepEquals(
    "simple obj1 should equal simple obj1",
    { foo: "bar" },
    { foo: "bar" }
  )
  var obj1 = { name: "dan", age: 22, stats: { s: 10, b: 20, c: 31 } }
  var obj2 = { name: "bob", age: 21, stats: { s: 10, b: 20, c: 30 } }
  assert.deepEquals("obj1 should equal obj2", obj1, obj2)

  assert.truthy("string '' is truthy", "")
  assert.truthy("string 'foo' is truthy", "foo")
  assert.falsey("0 is falsey", 0)
  assert.falsey("1 is falsey", 1)
  assert.falsey("null is falsey", null, true)
  assert.isMutable("Object is mutable", 10)
  assert.isImmutable("Number is immutable", { foo: 99 })
  assert.throwsError("Throws an error", "" + new Error())
  assert.throwsError("Throws an error", new Error())
})

// run the tests
run()

Using assert (object syntax)

The assert method can also passed an object.

Usage:

assert({
  message: "some string",
  actual: foo,
  expected: bar,
})

Examples:

/* tests using 'assert' with object syntax */

test("test using assert, object syntax", () => {
  assert({
    message: "10 should equal 10",
    expected: 10,
    actual: app.add(5, 5)
  })
  assert({
    message: "10 should equal 10 ...",
    expected: true,
    actual: app.add(5, 5) === 10
  })
  assert({
    message: 'assertion should assume expected=true (missing "expected" field)',
    actual: app.add(1, 1) === 2
  })
})

// run the tests
run()

Using t

Usage:

t.foo(actual [, expected])

Where foo can be any of these methods:

  • t.ok() - checks if something is truthy
  • t.notOk() - checks if something is falsey
  • t.equal() - uses the == comparison
  • t.strictEqual() - uses the === comparison
  • t.deepEqual() - uses the react-fast-compare deep equals comparison
  • t.throws() - checks if something throws an error
  • t.type() - check if something is of a specific type

(Note the difference to tap and tape - there's no msg as the third parameter)

/* using TAP style assertions */

test("test using t", () => {
  t.equal(1, 1)
  t.strictEqual(1, "1")
  t.deepEqual([1, 2, 3], [1, 2, 3])
  t.deepEqual([1, 2, 3], [1, 3, 4])
  t.deepEqual({ one: 1, foo: "baz" }, { one: 1, foo: "bar" })
  t.throws(new Error())
  t.throws("I'm just a String")
  t.type("I'm of type String", "function")
  t.type("I'm of type String", "string")
})

BDD style tests

Tests can be grouped arbitrarily, using the group function. Example:

/* group tests using the 'group' function */

group("First group of tests", () => {
  test("some message", () => {
    assert.strictEquals("one should equal one", 1, 1)
  })
  test("some message", () => {
    assert.strictEquals("one should equal one", 1, 1)
  })
})

The group function has these aliases: feature, scenario, describe and given.

The test function also has these aliases: it and when.

This means you can group your tests however you like, and write them in a number of styles:

/* jasmine or mocha style tests */

describe("Scenario: maths", () => {
  it("should add up the numbers:", () => {
    expect("1 + 1 equals 2", app.add(1, 1), 2)
    expect("1 + 1 equals 2 (fails)", app.add(1, 1), 3)
  })
})


/* some (made-up) gherkin style tests */

given("Scenario 3: some maths", () => {
  when("we add up the numbers:", () => {
    should("equal 2", app.add(1, 1), 2)
    should("equal 2 (fails)", app.add(1, 1), 3)
  })
})


/* larger BDD/gherkin/cucumber style tests */

feature("Calculator", () => {

  scenario("Addition and subtraction", () => {
    test("Subtractions", () => {
      expect("1 - 1 equals 0", demoApp.sub(1, 1) === 0, true)
      expect("1 - 1 equals 1", demoApp.sub(1, 1), 1)
    })
    test("Additions", () => {
      expect("1 + 1 equals 2", demoApp.add(1, 1), 2)
      expect("1 + 1 equals 2", demoApp.add(1, 1), 3)
    })
  })

  scenario("Some mathsy scenario", () => {
    test("Dividing numbers", () => {
      expect("10 / 2 equals 5", demoApp.div(10, 2), 5)
    })
    test("Working with nonsense", () => {
      // isNaN reports its type as number, so use this:
      assert.isType("1 + 10 is a number", 1 + 10, "number")
      // 1 + "foo" returns string
      assert.isType("1 + 'foo' is not a number", 1 + 'foo', "string")
      // 1 - "foo" returns isNaN
      assert.isType("1 - 'foo' is not a number", 1 - 'foo', "number", false)
    })
  })

})

The test results output will be indented appropriately, like so:

grouped and indented test results

No globals

Like many TAP-style test suites, tea can run without needing any globals or "magic" variables, that seem to come from nowhere.

To run tea in a way that does not use globals, see examples/no-globals-tests.js:

tea = require("../src/tea.js")

/*
 * Follow the TAP style of NOT creating any globals (except 'tea' itself)
 */

// tea() /* Dont init tea, then it won't register any globals */

/* OPTIONAL - set the default args directly */
tea.reportFormat = "tap"
tea.noIndent = true

/* OPTIONAL - alternatively, parse command line arguments (if any) */
tea.getArgs()

// Begin tests, without using globals

// using TAP style assertions - but no 'msg' as 3rd param to t()

tea.test("test one, using tea.t", t => {
  t.equal(1, 1)
  t.strictEqual(1, "1")
  t.deepEqual([1, 2, 3], [1, 2, 3])
})

tea.test("test two, using tea.t", t => {
  t.deepEqual([1, 2, 3], [1, 2, 3])
  t.deepEqual([1, 2, 3, 4], [4, 5])
  t.deepEqual({ one: 1, foo: "baz" }, { one: 1, foo: "bar" })
  t.throws(new Error())
  t.throws("I'm just a String")

  // using other assertions

  tea.expect("Foo is bar", "foo", "bar")
})

tea.run()

In summary, to avoid using globals with tea:

  • don't call tea() before your tests
  • call tea.test() instead of just test()
  • pass in t as a param to your tests...
  • ...or use tea.assert, tea.expect instead of assert, expect
  • call tea.run() instead of just run()

Integration tests

Running in a real browser

If setting up a headles browser is too much work, you can use tea to test stuff in a real page, using a regular old <script> tag:

<script src="https://unpkg.com/@scottjarvis/tea"></script>
<script>

  tea() // init tea

  test("check text in page", function(){
    var el = document.getElementById('#myElem')
    assert.strictEquals("elem contains correct text", el.innerText, "expected text")
  })

  run() // run tests

</script>

You can even copy and paste dist/tea.umd.js into the DevTools console directly! ...Then just write/paste some tests in the console and call run().

NOTE:

In the browser, passing command-line options like --quiet won't work, but you can set these options in the DevTools directly instead, and then call run():

  • tea.quiet = true|false
  • tea.args.verbose = true|false
  • tea.failFast = true|false
  • tea.reportFormat = 'console|tap|debug'

You'll be able to see the test results in the DevTools:

test results shown in DevTools

Debug with console.table()

If using the DevTools to see your test results, you can set tea.reportFormat = 'debug', to see your test results in a clickable, filterable console.table():

test results shown in DevTools, using

NOTE: If running tests using Node, you can set --format=debug to see the AssertionError stack traces ;)

Running in a headless browser

You might want to test your app in a real browser, without having to use <script> tags and the DevTools. Or, you might need to run tests in a real browser on a system that has no desktop - such as a continuous integration (CI) environment.

For these cases, you can use a 'headless browser'.

To do so, it's recommended to use GhostJS with tea. Install it like so:

npm i ghostjs --save-dev

GhostJS is a wrapper around the following headless browsers: phantomjs, chrome, slimerJS (FireFox).

It enables a nice syntax when dealing with headless browsers.

However, since ghostjs uses async/await you'll also need use babel to run your tests. At a minimum you should install these packages:

npm install babel-preset-es2015 --save-dev
npm install babel-preset-stage-0 --save-dev

In a file named .babelrc (in the root/top-level folder of your project):

{
  "presets": ["es2015", "stage-0"]
}

You can then use ghostjs async/await stuff inside your tests:

tea = require('tea');
ghost = require('ghostjs');

tea() // init tea

test("check the page loaded OK", async () => {
  // Get page title
  await ghost.open('http://google.com')
  let pageTitle = await ghost.pageTitle()
  assert.strictEquals(pageTitle, 'Google')

  // Get the content of the body
  let body = await ghost.findElement('body')
  assert.strictEquals(await body.isVisible(), true)
})

...

Run your GhostJS enabled tests like so:

ghostjs --browser phantom tests/*.js

NOTE: you can replace 'phantom' with 'chrome' or 'firefox'.

Making changes to tea

Look in src/tea.js, make any changes you like.

Rebuild the bundles in dist/ using this command: npm run build

To do

  • Add skip.test(...) to easily skip tests
  • Add more assertions:
    • assert.matchesRegex - ensure foo matches a regex assert.matchesRegex(msg, regex, expected)
    • etc
  • Document testing of the following:
  • Add simple, built-in integration testing support:
    • include wrapper/support for PhantomJS (see node-phantomjs-simple)
    • bundle phantomjs (32bit and 64bit builds)
  • Add better stack traces - resolve them and cut out the irrelevant stuff
  • Some kind of snapshotting

Pull Requests welcome ;)

Acknowledgements

  • microbundle - used to bundle tea into various builds (in dist/)
  • react-fast-compare - the deep equals function used in tea
  • fast-deep-equals - a faster alternative to the above
  • hyperon - borrowed functions for checking for React components in tea
  • ghostjs - a nice API for handling headless browsers
  • nightmareJS - a nice alternative to GhostJS that uses Electron as it's headless browser