Bruker:Jeblad/Module:BDD
- Note that the module is not ready for production, it is still under active development!
The purpose of this module is to support behavior-driven development (BDD), a software development process that emerged from test-driven development (TDD), in on-going development of Lua-based modules. It makes an assumption of a test module on a separate page, possibly a subpage, and presentation on another page like the talk page or generated through an API call.
The module mimics some ideas from other BDD libs, like RSpec [1], Jasmine [2], and Busted [3]. There are no clear standard on how to do this, so there are some variation and adaption.
The module is not built for great speed, it uses closures to build a number of small objects, that is methods returns a value that holds a closure. This creates a fairly efficient implementation for simple access, but it is not very efficient for caching larger structures. Some larger structures are put in tables and built once. This is done for Expect
where each new instance would otherwise create a lot of methods.
The module avoids as much caching as possible, as this can poison the tests.
Usage
[rediger | rediger kilde]If you have a module like «Module:HelloWorld», the ubiquitous and quite pesky example, coded as something like
local p = {}
function p.helloWorld()
return "Hi there!"
end
return p
Then on a test page you would test this like like the following
require 'Module:BDD'()
local p = require 'Module:HelloWorld'()
describe('Hello world', function()
context('On all pages', function()
it('says hello', function()
expect('p.helloWorld()', p.helloWorld()):toBe("Hi there!");
end);
end);
end);
return result('Module:HelloWorld')
In the first line the test framework is not only loaded, but it is also installed in the global name space. The trailing parenthesis is a call on the loaded table, and it will install additional global functions. It is not necessary to keep a pointer to the test framework, the functions keep their own pointers.
In the second line the module under test is loaded. Usually it is necessary to keep a pointer to the returned value.
The describe
(alt Given
) function adds a title and evaluates the function from a xpcall
(not done yet). The same happens with context
(alt When
) and it
(alt Then
). These three are really the same, it is only our interpretation that changes.
The expect
function adds a comment and the statement that creates the actual value. It then builds a closure and returns a structure in a flow style. This makes it possible to manipulate the instance, before we finally evaluates it and stores the result for later visualization. In this case we simply tests it to see if it is equal.
The final results are returned by a call to result
. Like before this can add a title.
The tests can be prepared for for localization into several languages, for example English and Norwegian Bokmål
require 'Module:BDD'()
local p = require 'Module:HelloWorld'()
describe({en='Hello world', nb='Hallo verden'}, function()
context({en='on all pages', nb='på alle sider'}, function()
it({en='says hello', nb='sier hallo'}, function()
expect('p.helloWorld()', p.helloWorld()):toBe("Hi there!");
end);
end);
end);
return result({ en='Module:HelloWorld', nb='Module:HelloWorld'})
In this case the languages will be chosen according to the site content language, and if that isn't defined it will use the languages from the fallback chain. If the first argument is a string, then that string will always be used. The language can be changed if the tests are run through the api, which makes it easy to check tests if they are prepared for the requested language.
Usually it is good practice to both use a common language to make the tests readable and reusable, and use a local language to make them readable for the local community at each project.
Further work
[rediger | rediger kilde]At present formatting does not work properly. It is expected to be fixed. ;)
Setup/teardown
[rediger | rediger kilde]There will be a solution whereby setup and teardown is chained. That means that the tests are somewhat isolated from each other at each test level, that is each time we define a describe, context, or it, the environment will be recreated. State can still leak between expectations though.
It is assumed that there will be no global setup and teardown as there is no resource allocation available from pages in Scribunto.
Xpcall
[rediger | rediger kilde]All calls to provided functions should be protected in xpcalls so a single exception does not make the whole test set to fail. A failed function should be marked as such in the report.
Actual function is xpcall, and it will always add a stack trace on our own stack. The entry might be without a message.
This is partially implemented, proper formatting remains unsolved.
Spy on public calls
[rediger | rediger kilde]Note that spying on public calls made before the module is available to the testing regime will not be possible.
- Carp
- Adds a message to stack without exiting the test, printing the callers name and its arguments.
- Cluck
- Like Carp, but also prints a stack trace starting one level up.
- Croak/Confess
- Like Carp, but also stops the running test (the user provided anonymous function). Because it throws an exception it will always trigger a stack trace.
Call would be like [carp|cluck|croak|confess](message, table, method)
.
Set up of spies are implemented, but they should be removed during teardown. Such cleanup must check if the struct is still available. This may happen if structs are created inside the test environment.
Coverage for public call
[rediger | rediger kilde]It should be possible to register spies for profiling coverage on members of a module. A function should loop over all members and register spies for all of them. Coverage can then be calculated for the public members. This will only give coverage of the entry points, but it seems sufficient to get a coarse number for coverage.
Stub public methods
[rediger | rediger kilde]It should be possible to stub public methods from the test page. This will not include private methods. (Is this something that should be part of a BDD module?)
Module return
[rediger | rediger kilde]At present an explicit return must be done. It seems to be possible to do an implicit return. This makes a slightly less error prone setup procedure.
Several options exist in literature, but it might be some limitations to what is actually allowed in Scribunto. It seems like all options are blocked.
Test Anything Protocol
[rediger | rediger kilde]At some point the Test Anything Protocol (TAP) should be supported. The simplest solution is to use yet another page for that response, but it is also possible to test on the page name. This can be different if we transclude the talk page into some other page, or even constructs the page on the fly through the api. If the transcluded page name is "API" then we switch to a TAP response. This makes it possible to support several formats from a single page.
Support libs
[rediger | rediger kilde]- Module:BDD/utils – Utility functions for testing tables and table contents. The functions are roughly equal to the same methods in Yonabas Moses library. [4]
- Module:BDD/view – View to show the result of the tests.
- Module:BDD/localize – Access methods to the localization libraries and the localized strings.
- Special:Prefixindex/Module:BDD/localize/ – The localized strings for each language.
See also
[rediger | rediger kilde]
-- module for Behavior Driven Development testing framework
-- © John Erling Blad, Creative Commons by Attribution 3.0
local util = require('Modul:BDD/utils')
local view = require('Modul:BDD/view')
-- @var The table holding the modules exported members
local bdd = {}
local function buildName( str )
return str:gsub("([A-Z])", function(str) return '-'..string.lower(str) end )
end
-- constructor for a closure to make the functions use the same stack
local function BDD()
-- @var This is the stack where we store our temporary formatters
local stack = { "result", '' }
-- @var This is the accumulators where we store our statistics
-- local statistics = { "statistics", { "accumulator" , 0, 0 } }
-- @var accumulator for failed tests
local numFailed = 0
-- @var base for expectations
local base = {}
-- @var list of preprocess functions
-- local preprocess = {}
-- function to make the chainable preprocess method
-- @param name identifier for the method
-- @param func the code to be run
-- @return self to make the method chainable
local function makePreProcess( name, func )
base[name] = function( self )
self.preprocess[1+#self.preprocess] = { buildName( name ), func }
return self
end
end
-- preprocess a string to make it all upper case
-- @param str string that will be upper cased
-- @return string that is upper cased
makePreProcess( 'asUpper',
function ( str )
return str:upper()
end
)
-- @alias upper
base.upper = base.asUpper
-- @alias asUC
base.asUC = base.asUpper
-- @alias uc
base.uc = base.asUpper
-- preprocess a string to make it all lowerr case
-- @param str string that will be lower cased
-- @return string that is lower cased
makePreProcess( 'asLower',
function ( str )
return str:lower()
end
)
-- @alias lower
base.lower = base.asLower
-- @alias asLC
base.asLC = base.asLower
-- @alias lc
base.lc = base.asLower
-- preprocess a string to make the first letter upper case
-- @param str string that will have the first letter upper cased
-- @return string with the first letter upper cased
makePreProcess( 'asUpperFirst',
function ( str )
return str:sub(1,1):upper()..str:sub(2)
end
)
-- @alias upperfirst
base.upperfirst = base.asUpperFirst
-- @alias asUCFirst
base.asUCFirst = base.asUpperFirst
-- @alias asUcFirst
base.asUcFirst = base.asUpperFirst
-- @alias asUCfirst
base.asUCfirst = base.asUpperFirst
-- @alias ucfirst
base.ucfirst = base.asUpperFirst
-- preprocess a string to make the first letter lower case
-- @param str string that will have the first letter lower cased
-- @return string with the first letter lower cased
makePreProcess( 'asLowerFirst',
function ( str )
return str:sub(1,1):lower()..str:sub(2)
end
)
-- @alias lowerfirst
base.lowerfirst = base.asLowerFirst
-- @alias asLCFirst
base.asLCFirst = base.asLowerFirst
-- @alias asLcFirst
base.asLcFirst = base.asLowerFirst
-- @alias asLCfirst
base.asLCfirst = base.asLowerFirst
-- @alias lcfirst
base.lcfirst = base.asLowerFirst
-- preprocess a string with an identity function
-- @param str string that will be the source for the identity
-- @return string that is the identity
makePreProcess( 'asIdentity',
function ( str )
return str
end
)
-- @var list of postprocess functions
--local postprocess = {}
-- function to make the chainable postprocess method
-- @param name identifier for the method
-- @param func the code to be run
-- @return self to make the method chainable
local function makePostProcess( name, func )
base[name] = function( self )
self.postprocess[1+#self.postprocess] = { buildName( name ), func }
return self
end
end
-- postprocess a value with an identity function
-- @param bool boolean that will be the source for the identity
-- @return boolean that is the identity
makePostProcess( 'whenIdentity',
function ( bool )
return bool
end
)
-- postprocess a value with an invert function
-- @param bool boolean that will be the source for the inversion
-- @return boolean that is the inverted
makePostProcess( 'whenInvert',
function ( bool )
return not bool
end
)
-- @alias invert
base.invert = base.whenInvert
-- @alias inverted
base.inverted = base.whenInvert
-- @alias whenInv
base.whenInv = base.whenInvert
-- @alias inv
base.inv = base.whenInvert
-- @var list of similarity functions
local similarity = {}
-- function to make the chainable similarity method
-- @param name identifier for the method
-- @param func the code to be run
-- @return self to make the method chainable
local function makeSimilarity( name, func )
similarity[name] = func
base[name] = function( self )
self.similarity = { buildName( name ), func }
return self
end
end
-- similarity of the type test with boolean type
-- @param optional any value that will be the source (actual) value for the test
-- @return boolean that is the result of the type test
makeSimilarity( 'ifBoolean',
function ( a )
return type( a ) == 'boolean'
end
)
-- @alias Boolean
base.ifboolean = base.ifBoolean
-- similarity of the type test with string type
-- @param optional any value that will be the source (actual) value for the test
-- @return boolean that is the result of the type test
makeSimilarity( 'ifString',
function ( a )
return type( a ) == 'string'
end
)
-- @alias String
base.ifstring = base.ifString
-- similarity of the type test with number type
-- @param optional any value that will be the source (actual) value for the test
-- @return boolean that is the result of the type test
makeSimilarity( 'ifNumber',
function ( a )
return type( a ) == 'number'
end
)
-- @alias Number
base.ifnumber = base.ifNumber
-- similarity of the type test with table type
-- @param optional any value that will be the source (actual) value for the test
-- @return boolean that is the result of the type test
makeSimilarity( 'ifTable',
function ( a )
return type( a ) == 'table'
end
)
-- @alias Table
base.iftable = base.ifTable
-- similarity of the type test with nil type
-- @param optional any value that will be the source (actual) value for the test
-- @return boolean that is the result of the type test
makeSimilarity( 'ifNil',
function ( a )
return type( a ) == 'nil'
end
)
-- @alias Nil
base.ifnil = base.ifNil
-- similarity method that will always fail
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifFail',
function ( a, b )
return false
end
)
-- @alias Fail
base.iffail = base.ifFail
-- similarity done as an equal operation between the first and second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifEqual',
function ( a, b )
return a == b
end
)
-- @alias equal
base.equal = base.ifEqual
-- similarity done as a deep equal operation between the first and second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifDeepEqual',
function ( a, b )
return util.deepEqual( a, b)
end
)
-- @alias deepequal
base.deepequal = base.ifDeepEqual
-- similarity done as a lesser than operation between the first and second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifLesserThan',
function ( a, b )
return a < b
end
)
-- @alias ifLesser
base.ifLesser = base.ifLesserThan
-- @alias ifLT
base.ifLT = base.ifLesserThan
-- @alias lesser
base.lesser = base.ifLesserThan
-- similarity done as a greater than operation between the first and second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifGreaterThan',
function ( a, b )
return a > b
end
)
-- @alias ifGreater
base.ifGreater = base.ifGreaterThan
-- @alias ifGT
base.ifGT = base.ifGreaterThan
-- @alias greater
base.greater = base.ifGreaterThan
-- similarity done as a lesser or equal than operation between the first and second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifLesserOrEqual',
function ( a, b )
return a <= b
end
)
-- @alias ifLesserEqual
base.ifLesserEqual = base.ifLesserOrEqual
-- @alias ifLE
base.ifLE = base.ifLesserOrEqual
-- @alias lesserequal
base.lesserequal = base.ifLesserOrEqual
-- similarity done as a greater or equal than operation between the first and second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifGreaterOrEqual',
function ( a, b )
return a >= b
end
)
-- @alias ifGreaterEqual
base.ifGreaterEqual = base.ifGreaterOrEqual
-- @alias ifGE
base.ifGE = base.ifGreaterOrEqual
-- @alias greaterequal
base.greaterequal = base.ifGreaterOrEqual
-- similarity done as a strict boolean false on the argument
-- @param optional any value that will be the source (actual) value for the type-value test
-- @return boolean that is the result of the test
makeSimilarity( 'ifFalse',
function ( a )
return type(a) == 'boolean' and a == false
end
)
-- @alias False
base.False = base.ifFalse
-- similarity done as a strict boolean true on the argument
-- @param optional any value that will be the source (actual) value for the type-value test
-- @return boolean that is the result of the test
makeSimilarity( 'ifTrue',
function ( a )
return type(a) == 'boolean' and a == true
end
)
-- @alias True
base.True = base.ifTrue
-- similarity done as a loose boolean false on the argument
-- @param optional any value that will be the source (actual) value for the value test
-- @return boolean that is the result of the test
makeSimilarity( 'ifFalsy',
function ( a )
return not a
end
)
-- @alias falsy
base.falsy = base.ifFalsy
-- similarity done as a loose boolean true on the argument
-- @param optional any value that will be the source (actual) value for the value test
-- @return boolean that is the result of the test
makeSimilarity( 'ifTruthy',
function ( a )
return not not a
end
)
-- @alias truthy
base.truthy = base.ifTruthy
-- fake similarity done as an identity on the first argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return any that is the first argument
makeSimilarity( 'ifFirstIdentity',
function ( a, b )
return a
end
)
-- @alias firstIdentity
base.firstIdentity = base.ifFirstIdentity
-- fake similarity done as an identity on the second argument
-- @param optional any value that will be the source (actual) value for the comparison
-- @param optional any value that will be the target (expected) value for the comparison
-- @return any that is the second argument
makeSimilarity( 'ifSecondIdentity',
function ( a, b )
return b
end
)
-- @alias secondIdentity
base.secondIdentity = base.ifSecondIdentity
-- similarity done as a string match for the target inside the source string
-- @param optional any value that will be the source (actual) value
-- @param optional any value that will be the target (expected) value
-- @return string that is the first result from the match
makeSimilarity( 'ifMatch',
function ( a, b ) local res = string.match(a, b);
return res or false
end
)
-- @alias match
base.match = base.ifMatch
-- similarity done as an ustring match for the target inside the source string
-- @param optional any value that will be the source (actual) value
-- @param optional any value that will be the target (expected) value
-- @return string that is the first result from the match
makeSimilarity( 'ifUstringMatch',
function ( a, b ) local res = mw.ustring.match(a, b);
return res or false
end
)
-- @alias umatch
base.umatch = base.ifUstringMatch
-- similarity done as a search for the target inside the source table
-- @param optional any value that will be the source (actual) value
-- @param optional any value that will be the target (expected) value
-- @return [false|number] that is the position of the found item, or false
makeSimilarity( 'ifContains',
function ( a, b )
return util.contains( a, b )
end
)
-- @alias ifContain
base.ifContain = base.ifContains
-- @alias contains
base.contains = base.ifContains
-- @alias contain
base.contain = base.ifContains
-- similarity done as a test for the source value inside the limits of the target
-- @param optional any value that will be the source number (actual) value
-- @param optional any value that will be the target number (expected) value
-- @param optional any value that will be the error on the target value
-- @return boolean that is the result of the comparison
makeSimilarity( 'ifCloseTo',
function ( a, b, c )
return b-c <= a and a <= b+c
end
)
-- @alias closeTo
base.closeTo = base.ifCloseTo
-- @var list of similarity functions
local matchers = {}
-- function to make the chainable matcher method
-- @param name identifier for the method
-- @param func the code to be run
-- @return self to make the method chainable
local function makeMatcher( name, func )
base[name] = function( self, ... )
if func then
self.similarity = { buildName( name ), func }
end
local res = self.exec( ... )
-- @todo must be wrapped up and made html safe
-- str comes from the constructor
local rep = { "expect", self.str, { "params", ... }, {}, unpack( res ) }
if not res then
numFailed = 1+numFailed
--rep[1+#rep] = debug.traceback()
end
stack[1+#stack] = rep
return self
end
end
-- matching done with the stored operation between actual and expected value
-- the default match is with an equal operation
-- @param optional any value that will be the target (expected) value for the comparison
-- @return boolean that is the result of the comparison
makeMatcher(
'toBe',
false
)
-- @alias be
base.be = base.toBe
-- @alias beEqual
base.beEqual = base.toBe
-- @alias toEqual
base.toEqual = base.toBe
-- matching done with a type operation on the actual and 'boolean' as expected value
-- @return boolean that is the result of the type test
makeMatcher(
'toBeBoolean',
similarity['ifBoolean']
)
-- @alias beBoolean
base.beBoolean = base.toBeBoolean
-- @alias toBoolean
base.toBoolean = base.toBeBoolean
-- @alias Boolean
base.Boolean = base.toBeBoolean
-- matching done with a type operation on the actual and 'string' as expected value
-- @return boolean that is the result of the type test
makeMatcher(
'toBeString',
similarity['ifString']
)
-- @alias beString
base.beString = base.toBeString
-- @alias toString
base.toString = base.toBeString
-- @alias String
base.String = base.toBeString
-- matching done with a type operation on the actual and 'number' as expected value
-- @return boolean that is the result of the type test
makeMatcher(
'toBeNumber',
similarity['ifNumber']
)
-- @alias beNumber
base.beNumber = base.toBeNumber
-- @alias toNumber
base.toNumber = base.toBeNumber
-- @alias Number
base.Number = base.toBeNumber
-- matching done with a type operation on the actual and 'table' as expected value
-- @return boolean that is the result of the type test
makeMatcher(
'toBeTable',
similarity['ifTable']
)
-- @alias beTable
base.beTable = base.toBeTable
-- @alias toTable
base.toTable = base.toBeTable
-- @alias Table
base.Table = base.toBeTable
-- matching done with a type operation on the actual and 'nil' as expected value
-- @return boolean that is the result of the type test
makeMatcher(
'toBeNil',
similarity['ifNil']
)
-- @alias beNil
base.beNil = base.toBeNil
-- @alias toNil
base.toNil = base.toBeNil
-- @alias Nil
base.Nil = base.toBeNil
-- matching done with a deep equality operation on the actual and expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeDeepEqual',
similarity['ifDeepEqual']
)
-- @alias beDeepEqual
base.beDeepEqual = base.toBeDeepEqual
-- @alias
base.toDeepEqual = base.toBeDeepEqual
-- matching done in a way that will always fail
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeFailing',
similarity['ifFail']
)
-- @alias beFailing
base.beFailing = base.toBeFailing
-- @alias toFailing
base.toFailing = base.toBeFailing
-- @alias Failing
base.Failing = base.toBeFailing
-- matching done with a lesser than operation on the actual and expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeLesserThan',
similarity['ifLesserThan']
)
-- matching done with a greater than operation on the actual and expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeGreaterThan',
similarity['ifGreaterThan']
)
-- matching done with a lesser or equal operation on the actual and expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeLesserOrEqual',
similarity['ifLesserOrEqual']
)
-- matching done with agreater or equal operation on the actual and expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeGreaterOrEqual',
similarity['ifGreaterOrEqual']
)
-- matching done with a loose equality operation on the actual and 'false' as expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeFalsy',
similarity['ifFalsy']
)
-- @alias beFalsy
base.beFalsy = base.toBeFalsy
-- @alias toFalsy
base.toFalsy = base.toBeFalsy
-- @alias Falsy
base.Falsy = base.toBeFalsy
-- matching done with a loose equality operation on the actual and 'true' as expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeTruthy',
similarity['ifTruthy']
)
-- @alias beTruthy
base.beTruthy = base.toBeTruthy
-- @alias toTruthy
base.toTruthy = base.toBeTruthy
-- @alias Truthy
base.Truthy = base.toBeTruthy
-- matching done with an equality operation on the actual and 'false' as expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeFalse',
similarity['ifFalse']
)
-- @alias beFalse
base.beFalse = base.toBeFalse
-- @alias toFalse
base.toFalse = base.toBeFalse
-- @alias False
base.False = base.toBeFalse
-- matching done with an equality operation on the actual and 'true' as expected value
-- @param optional any value that will be the target (expected) value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeTrue',
similarity['ifTrue']
)
-- @alias beTrue
base.beTrue = base.toBeTrue
-- @alias toTrue
base.toTrue = base.toBeTrue
-- @alias True
base.True = base.toBeTrue
-- fake matching that returns the actual value
-- @param optional any value that will be thrown away
-- @return any value that is used as the actual
makeMatcher(
'toBeFirstIdentity',
similarity['ifFirstIdentity']
)
-- fake matching that returns the expected value
-- @param optional any value that will be thrown away
-- @return any value that is used as the actual
makeMatcher(
'toBeSecondIdentity',
similarity['ifSecondIdentity']
)
-- matching done with a match for the target (expected) inside source (actual) string
-- @param optional any value that will be the target (expected) for the match
-- @return string that is the first result from the match
makeMatcher(
'toBeMatch',
similarity['ifMatch']
)
-- @alias beMatch
base.beMatch = base.toBeMatch
-- @alias toMatch
base.toMatch = base.toBeMatch
-- matching done with an ustring match for the target (expected) inside source (actual) string
-- @param optional any value that will be the target (expected) for the match
-- @return string that is the first result from the match
makeMatcher(
'toBeUstringMatch',
similarity['ifUstringMatch']
)
-- @alias beUMatch
base.beUMatch = base.toBeUstringMatch
-- @alias toUMatch
base.toUMatch = base.toBeUstringMatch
-- matching done as a search for the target inside the source table
-- @param optional any value that will be the target (expected) for the match
-- @return [false|number] that is the position of the found item, or false
makeMatcher(
'toBeContains',
similarity['ifContains']
)
-- @alias toContains
base.toContains = base.toBeContains
-- @alias toContain
base.toContain = base.toBeContains
-- @alias beContains
base.beContains = base.toBeContains
-- @alias beContain
base.beContain = base.toBeContains
-- matching done as a test for the source value inside the limits of the target
-- @param optional any value that will be the target number (expected) value
-- @param optional any value that will be the error on the target value
-- @return boolean that is the result of the comparison
makeMatcher(
'toBeCloseTo',
similarity['ifCloseTo']
)
-- @alias toCloseTo
base.toCloseTo = base.toBeCloseTo
-- @alias beCloseTo
base.beCloseTo = base.toBeCloseTo
-- constructor for expectations
local function Expect( str, actual )
local self = {}
setmetatable(self, {__index = base})
-- @var list of processes to run before matching
self.preprocess = {}
self.str = str
self.actual = actual
-- @var similarity process
self.similarity = {
buildName('ifEqual'),
-- following is a default
function ( a, b ) return a == b end
}
-- @var list of processes to run after matching
self.postprocess = {}
function self.exec( ... )
local values = {}
-- this is the actual argument
values[1+#values] = { 'actual', self.actual }
--this is a chain of preprocesses
for _,v in ipairs( self.preprocess ) do
values[1+#values] = { v[1], v[2]( values[#values][2] ) }
end
local keep = #values
-- this is the anticipated result
values[1+#values] = { 'anticipated', ... }
-- this is the actual similarity test
values[1+#values] = { self.similarity[1], self.similarity[2]( values[keep][2], ... ) }
-- this is a chain of postprocesses
for _,v in ipairs( self.postprocess ) do
values[1+#values] = { v[1], v[2]( values[#values][2] ) }
end
return values
end
-- match for "throw"
-- @return table
function self.toThrow()
return self
end
-- match for "have been called"
-- @return table
function self.toHaveBeenCalled()
return self
end
-- match for "have been called with"
-- @return table
function self.toHaveBeenCalledWith()
return self
end
-- this is our instance
return self
end
-- @var This is the module structure returned from the test set
local wrap = {
-- the test runner
tests = function( frame )
--return mw.dumpObject( stack )
return view.Create( stack, {numfailed=numFailed} ).format()
end,
categories = function(frame)
local cats = {
'[[Category:category-all-tests]]',
--'[[Category:'..g('category-all-tests')..']]',
}
return table.concat(cats, '\n')
end
}
local function stackTrace( msg )
local tbl = {}
tbl[1+#tbl] = "stack"
tbl[1+#tbl] = msg
-- @todo must be made html safe
tbl[1+#tbl] = mw.text.split( debug.traceback(), "\n\t", true )
stack[1+#stack] = tbl
end
local function exceptionTrace( msg )
local tbl = {}
tbl[1+#tbl] = "exception"
tbl[1+#tbl] = msg
-- @todo must be made html safe
tbl[1+#tbl] = mw.text.split( debug.traceback(), "\n\t", true )
stack[1+#stack] = tbl
end
-- this builds a frame and later wraps it up
local function frame( name )
-- build and return an appropriate function
return function( str, func )
-- statistics[1+#statistics] = { "accumulator", 0, 0 }
local depth = 1+#stack
stack[1+#stack] = name
stack[1+#stack] = str
xpcall( func, exceptionTrace )
local tbl = {}
for i=depth,#stack,1 do
tbl[1+#tbl] = stack[i]
stack[i] = nil
end
-- local last = statistics[#statistics]
--table.insert(tbl, 3, last)
--local prev = statistics[#statistics-1]
--prev[2] = prev[2]+last[2]
--prev[3] = prev[3]+last[3]
-- table.remove(statistics)
stack[1+#stack] = tbl
end
end
-- this builds a xframe and later wraps it up
local function xframe( name )
-- build and return an appropriate function
return function( str, func )
stack[1+#stack] = { name, str }
end
end
-- this builds spies by monkey patching
-- note that it seems impossible to store at a funcs location
-- @todo still needs to clean it up in a teardown, now it sticks
local function spy( name, trace )
-- build and return an appropriate function
return function( str, struct, meth )
local oldFunc = struct[meth]
struct[meth] = function(...)
-- @todo must be wrapped up and made html safe
stack[1+#stack] = { name, str, { "params", ... } }
if name == 'cluck' then
stackTrace( name )
elseif name == 'croak' then
error('croak')
end
return oldFunc(...)
end
end
end
-- @var This is the functions exposed for the tests
local members = {
Given = frame('given'),
When = frame('when'),
Then = frame('then'),
describe = frame('describe'),
context = frame('context'),
it = frame('it'),
test = it,
pending = xframe('pending'),
xGiven = frame('xgiven'),
xWhen = frame('xwhen'),
xThen = frame('xthen'),
xdescribe = xframe('xdescribe'),
xcontext = xframe('xcontext'),
xit = xframe('xit'),
xtest = xit,
-- after,
-- before,
expect = Expect,
carp = spy("carp"),
cluck = spy("cluck", true),
croak = spy("croak"),
confess = croak,
-- @todo not sure about this
view = view.Create,
result = function(head)
stack[2] = head
return wrap
end,
stack = function()
return stack
end,
similarity = function()
return similarity
end
}
return members
end
-- metatable for the export
local mt = {
-- adjust the installation of the module
__call = function ()
_G['_BDD_TEST'] = true
local t = BDD()
for k,v in pairs(t) do
_G[k] = v
end
return bdd
end
}
-- install the metatable
setmetatable(bdd, mt)
return bdd