Hopp til innhold

Bruker:Jeblad/Module:BDD

Fra Wikipedia, den frie encyklopedi
Dokumentasjon
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

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

At present formatting does not work properly. It is expected to be fixed. ;)

Setup/teardown

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

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

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

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

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

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

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

See also


-- module for Behavior Driven Development testing framework
-- © John Erling Blad, Creative Commons by Attribution 3.0

-- @var The table holding this modules exported members
local bdd = {}

local idrunner = (function()
	local counter = 0
	return function()
		counter = 1+counter
		return tostring( counter )
	end
end)()
--idrunner = idrunner()

-- escape a string to make it a valid name for classes and add a prefix
local function escClass( str, pref )
	if str then
		str = string.gsub( str, "[%s]", "_" )
		str = string.gsub( str,
			"([^%w%-%_])",
			function( char )
				return string.format ( "$%02X", string.byte( char ) )
			end
		)
	end
	return (pref or 'bdd') .. '-' .. str	
end

-- raw count of all the items in the provided table
local function count( t )
  local i = 0
    for k,v in pairs( t ) do i = i + 1 end
  return i
end

-- size based on the raw count
local function size(...)
	local args = {...}
	local arg1 = args[1]
	if arg1 == nil then
		return 0
	elseif type( arg1 ) == 'table' then
		return count( args[1] )
	else
		return count( args )
	end
end

-- deep equal of two objects
local function deepEqual( objA, objB, useMt )
	local typeObjA = type( objA )
	local typeObjB = type( objB )

	if typeObjA ~= typeObjB then
		return false end
	if typeObjA ~= 'table' then
		return objA == objB
	end

	local mtA = getmetatable( objA )
	local mtB = getmetatable( objB )

	if useMt then
		if (mtA or mtB) and (mtA.__eq or mtB.__eq) then
			return mtA.__eq(objA, objB) or mtB.__eq(objB, objA) or (objA==objB)
		end
	end

	if size( objA ) ~= size( objB ) then
		return false
	end

	for i,v1 in pairs( objA ) do
		local v2 = objB[i]
		if v2 == nil or not deepEqual( v1, v2, useMt ) then
			return false
		end
	end

	for i,v1 in pairs(objB) do
		local v2 = objA[i]
		if v2 == nil then
			return false
		end
	end

	return true
end

function contains(t, arg)
	local cmp = (type(arg)=='function') and arg or deepEqual
	for k,v in pairs(t) do
		if cmp(v, arg) then
			return k
		end
	end
	return false
end

local function View( tbl, stats )
	local self = {}
	
	local truncateLength = 30

	-- @var _params to be used unless there are local overrides
	local messages = {}
	messages['result'] = 'Result $1'
	messages['pending'] = 'Pending $1'
	messages['describe'] = 'Describe $1'
	messages['context'] = 'Context $1'
	messages['it'] = 'It $1'
	messages['xdescribe'] = 'Mute describe $1'
	messages['xcontext'] = 'Mute context $1'
	messages['xit'] = 'Mute it $1'
	messages['exception'] = 'Catch $1'
	messages['stack'] = 'Stack'
	messages['carp'] = 'Carp'
	messages['cluck'] = 'Cluck'
	messages['croak'] = 'Croak'
	messages['actual'] = 'actual ($1)'
	--messages['actual-noargs'] = 'actual is empty'
	messages['actual-noargs'] = ''
	messages['anticipated'] = 'and anticipate ($1),'
	--messages['anticipated-noargs'] = 'and anticipate is empty'
	messages['anticipated-noargs'] = ''
	messages['expect-args'] = 'Expect $1'
	messages['expect-no-formatter'] = 'Expect $1'
	messages['expect'] = 'Expect $1'
	messages['as-upper'] = 'as uppercase'
	messages['as-lower'] = 'as lowercase'
	messages['as-upper-first'] = 'as uppercase first'
	messages['as-lower-first'] = 'as lowercase first'
	messages['as-identity'] = 'as identity "$1"'
	messages['when-invert'] = 'and then invert ($1)'
	messages['when-identity'] = 'and then as identity "$1"'
	messages['if-boolean'] = ', it is boolean type'
	messages['if-string'] = ', it is string type'
	messages['if-number'] = ', it is number type'
	messages['if-table'] = ', it is table type'
	messages['if-nil'] = 'it is nil'
	messages['if-fail'] = ', force fail' --change
	messages['if-equal'] = 'compare with equal ($1)'
	messages['if-deep-equal'] = 'compare with deep equal ($1)'
	messages['if-lesser-than'] = 'if lesser than, then'
	messages['if-greater-than'] = 'if greather than, then'
	messages['if-lesser-or-equal'] = 'if lesser or equal, then'
	messages['if-greater-or-equal'] = 'if greater or equal, then'
	messages['if-false'] = 'compare with false'
	messages['if-true'] = 'compare with true'
	messages['if-falsy'] = 'compare with falsy'
	messages['if-truthy'] = 'compare with truthy'
	messages['if-first-identity'] = 'first identity to be "$1"'
	messages['if-second-identity'] = 'second identity to be "$1"'
	messages['if-match'] = 'if it match ($1)'
	messages['if-match-noargs'] = 'if it match nothing'
	messages['if-ustring-match'] = 'if it match ($1)'
	messages['if-ustring-match-noargs'] = 'if it match nothing'
	messages['if-contains'] = 'if it contains value ($1)'
	messages['if-close-to'] = 'if value is close ($1)'
	messages['to-be-boolean'] = 'to be a boolean'
	messages['to-be-string'] = 'to be a string'
	messages['to-be-number'] = 'to be a number'
	messages['to-be-table'] = 'to be a table'
	messages['to-be-nil'] = 'to be a nil'
	messages['to-be'] = 'to become "$1"'
	messages['to-be-failing'] = 'to be failing'
	--messages['to-be-equal'] = 'to be equal ($1)'
	messages['to-be-deep-equal'] = 'to be deep equal ($1)'
	messages['to-be-lesser-than'] = 'to be lesser than "$1"'
	messages['to-be-greater-than'] = 'to be greater than'
	messages['to-be-lesser-or-equal'] = 'to be lesser or equal'
	messages['to-be-greater-or-equal'] = 'to be greater or equal'
	messages['to-be-falsy'] = 'to be falsy'
	messages['to-be-truthy'] = 'to be truthy'
	messages['to-be-false'] = 'to be false'
	messages['to-be-true'] = 'to be true'
	messages['to-be-first-identity'] = 'first identity to be "$1"'
	messages['to-be-second-identity'] = 'second identity to be "$1"'
	messages['to-be-match'] = 'to match ($1)'
	messages['to-be-match-noargs'] = 'to match nothing'
	messages['to-be-ustring-match'] = 'to match ($1)'
	messages['to-be-ustring-match-noargs'] = 'to match nothing'
	messages['to-be-contains'] = 'to contain value ($1)'
	messages['to-be-close-to'] = 'to be close to value ($1)'
	messages['joiner'] = ', '
	messages['no-formatter'] = "[no-formatter:$1]"
	messages['skipped'] = "[skipped]"
	messages['category-all-tests'] = "All tests"
	messages['category-failed-tests'] = "Failed tests"
	messages['category-good-tests'] = "Good tests"
	
	local function g( key, ...)
		local msg = mw.message.new( 'bdd-' .. key )
		if msg:isBlank() then
			assert(messages[key],
				"invalid message: " .. key .. " is not a defined string" )
			msg = mw.message.newRawMessage( messages[key] )
		end
		return msg
	end
	
	local formatters = {}
	
	local function _xdefinition( ... )
		local t = {...}
		local dt = mw.html.create( 'dt' )
		local dl = mw.html.create( 'dl' )
		:node( dt )
		for i,v in ipairs({...}) do
			if i == 1 then
				dt:addClass( escClass( v ) )
			elseif i == 2 then
				dt:wikitext(
					g( t[1] )
					:params( v )
					:plain()
				)
			end
		end
		return dl
	end

	local function _definition( ... )
		local t = {...}
		local id = idrunner()
		local dl = mw.html.create( 'dl' )
		--dl:addClass( 'mw-collapsible' )
		local dt = mw.html.create( 'dt' )
		dt:addClass( 'mw-customtoggle-' .. id )
		--dt:addClass( 'mw-collapsible' )
		local dd = mw.html.create( 'dd' )
		dd:addClass( 'mw-collapsible' )
		dd:attr( 'id', 'mw-customcollapsible-' .. id )
		for i,v in ipairs(t) do
			if i == 1 then
				dt:addClass( escClass( v ) )
			elseif i == 2 then
				dt:wikitext(
					g( t[1] )
					:params( v )
					:plain()
				)
			--elseif i == 3 then
				--local msg = g( t[1] )
				--dt:wikitext(msg:params( v ):plain() )
				--dt:wikitext(v[1])
			elseif v and v[1] then
				if type(formatters[v[1]]) == 'function' then
					local html = formatters[v[1]]( unpack( v ) )
					dd:node(
						html
					)
				else
					dd:wikitext(
						g( 'no-formatter' )
						:params(v[1] or 'none')
						:plain()
					)
				end
			else
				dd:wikitext(
					g( 'skipped' )
					:plain()
				)
			end
		end
	-- possible, but not sure this is a good idea
	--	if #t > 2 then
	--		dd:addClass( 'mw-collapsible' )
	--		if t[1] ~= 'describe' and t[1] ~= 'result' then
	--			dd:addClass( 'mw-collapsed' )
	--		end
	--	end
		dl:node( dt )
		dl:node( dd )
		return dl
	end

	local function _expect( ... )
		local t = {...}
		local ul = mw.html.create( 'ul' )
		for i,v in ipairs(t) do
			if i == 1 then
				ul:addClass( escClass( v ) )
				local last = t[#t]
				local final = last[#last]
				ul:addClass(
					final and 'bdd-expect-good' or 'bdd-expect-fail'
				)
			elseif i == 2 then
				-- delay this
			elseif i == 3 then
				local li = mw.html.create( 'li' )
				if v then
					if type(formatters[v[1]]) == 'function' then
						local html = formatters[v[1]]( unpack( v ) )
						if html then
							li:wikitext(
								g( 'expect-args' )
								:rawParams( mw.text.nowiki( t[2] ) )
								:rawParams( html:done() )
								:plain()
							)
						else
							li:wikitext(
								g( 'expect' )
								:rawParams( mw.text.nowiki( t[2] ) )
								:plain()
							)
						end
					else
						li:wikitext(
							g( 'expect-no-formatter' )
							:rawParams( mw.text.nowiki( t[2] ) )
							:plain()
						)
					end
				else
					li:wikitext(
						g( 'expect' )
						:rawParams( mw.text.nowiki( t[2] ) )
						:plain()
					)
				end
				ul:node(li)
			elseif i == 4 then
				-- skip this for now, it is the statistics
			elseif v and v[1] then
				local li = mw.html.create( 'li' )
				if type(formatters[v[1]]) == 'function' then
					local html = formatters[v[1]]( unpack( v ) )
					li:node( html )
				else
					li:wikitext(
						g( 'no-formatter' )
						:params(v[1] or 'none')
						:plain()
					)
				end
				ul:node(li)
			else
				local li = mw.html.create( 'li' )
				dd:wikitext(
					g( 'skipped' )
					:plain()
				)
				ul:node(li)
			end
		end
		return ul
	end

	local function dumpBox( obj )
		local dump = mw.dumpObject( obj )
		local short = mw.html.create( 'div' )
		short
			:addClass( 'bdd-popup-handle' )
			:css( 'display', 'inline-block' )
			:wikitext(
				mw.text.nowiki(
					mw.text.trim(
						mw.text.truncate( dump:gsub('\n', ' '), truncateLength, nil, true )
					)
				)
			)
		local box = mw.html.create( 'div' )
		box
			:addClass( 'bdd-popup-box' )
			:css('display', 'none')
		local full = mw.html.create( 'pre' )
		full
			:wikitext(
				mw.text.nowiki(
					mw.text.trim(
						dump
					)
				)
			)
		box:node(full)
		short:node(box)
		return short
	end

	local function _partial( ... )
		local t = {...}
		local li = mw.html.create( 'li' )
		local html
		if #t == 1 then
			-- empty params
		elseif #t == 2 then
			-- single params
			html = dumpBox( t[2] )
		else
			-- several params
		end
		if html then
			li:wikitext(
				g( t[1] )
				:rawParams( html )
				:plain()
			)
		else
			li:wikitext(
				g( t[1]..'-noargs' )
				:plain()
			)
		end
		
		return li
	end
	

	local function _params( ... )
		local t = {...}
		local html
		if #t == 1 then
			-- empty params
		elseif #t == 2 then
			-- single params
			html = dumpBox( t[2] )
		else
			-- several params
		end
		return html
	end
	
	local function _spy( ... )
		local t = {...}
		local ul = mw.html.create( 'ul' )
		for i,v in ipairs(t) do
			if i == 1 then
				ul:addClass( escClass( v ) )
				local last = t[#t]
				local final = last[#last]
				ul:addClass(
					final and 'bdd-spy-good' or 'bdd-spy-fail'
				)
			elseif i == 2 then
				-- delay this
			elseif i == 3 then
				local li = mw.html.create( 'li' )
				if v then
					if type(formatters[v[1]]) == 'function' then
						local html = formatters[v[1]]( unpack( v ) )
						if html then
							li:wikitext(
								g( 'spy-args' )
								:rawParams( mw.text.nowiki( t[2] ) )
								:rawParams( html:done() )
								:plain()
							)
						else
							li:wikitext(
								g( 'spy' )
								:rawParams( mw.text.nowiki( t[2] ) )
								:plain()
							)
						end
					else
						li:wikitext(
							g( 'spy-no-formatter' )
							:rawParams( mw.text.nowiki( t[2] ) )
							:plain()
						)
					end
				else
					li:wikitext(
						g( 'spy' )
						:rawParams( mw.text.nowiki( t[2] ) )
						:plain()
					)
				end
				ul:node(li)
			elseif i == 4 then
				-- skip this for now, it is the statistics
			elseif v and v[1] then
				local li = mw.html.create( 'li' )
				if type(formatters[v[1]]) == 'function' then
					local html = formatters[v[1]]( unpack( v ) )
					li:node( html )
				else
					li:wikitext(
						g( 'no-formatter' )
						:params(v[1] or 'none')
						:plain()
					)
				end
				ul:node(li)
			else
				local li = mw.html.create( 'li' )
				dd:wikitext(
					g( 'skipped' )
					:plain()
				)
				ul:node(li)
			end
		end
		return ul
	end

	local function _nop( ... )
		local placeholder = mw.html.create( 'div' )
			:css( 'display', 'none' )
		return placeholder
	end
	
	formatters['result'] = _definition
	formatters['describe'] = _definition
	formatters['context'] = _definition
	formatters['it'] = _definition
	formatters['xdescribe'] = _xdefinition
	formatters['xcontext'] = _xdefinition
	formatters['xit'] = _xdefinition
	formatters['expect'] = _expect
	formatters['results'] = _results
	formatters['params'] = _params
	formatters['carp'] = _spy
	formatters['cluck'] = _spy
	formatters['croak'] = _spy
	formatters['actual'] = _partial
	formatters['anticipated'] = _partial
	formatters['as-upper'] = _partial
	formatters['as-lower'] = _partial
	formatters['as-upper-first'] = _partial
	formatters['as-lower-first'] = _partial
	formatters['when-invert'] = _partial
	formatters['if-boolean'] = _partial
	formatters['if-string'] = _partial
	formatters['if-number'] = _partial
	formatters['if-table'] = _partial
	formatters['if-nil'] = _partial
	formatters['if-fail'] = _partial
	formatters['if-equal'] = _partial
	formatters['if-deep-equal'] = _partial
	formatters['if-lesser-than'] = _partial
	formatters['if-greater-than'] = _partial
	formatters['if-lesser-or-equal'] = _partial
	formatters['if-greater-or-equal'] = _partial
	formatters['if-false'] = _partial
	formatters['if-true'] = _partial
	formatters['if-falsy'] = _partial
	formatters['if-truthy'] = _partial
	formatters['if-match'] = _partial
	formatters['if-ustring-match'] = _partial
	formatters['if-contains'] = _partial
	formatters['if-close-to'] = _partial
	formatters['to-be'] = _partial
	formatters['to-be-failing'] = _partial
	formatters['to-be-deep-equal'] = _partial
	formatters['to-be-lesser-than'] = _partial
	formatters['to-be-greater-than'] = _partial
	formatters['to-be-lesser-or-equal'] = _partial
	formatters['to-be-greater-or-equal'] = _partial
	formatters['to-be-falsy'] = _partial
	formatters['to-be-truthy'] = _partial
	formatters['to-be-nil'] = _partial
	formatters['to-be-boolean'] = _partial
	formatters['to-be-string'] = _partial
	formatters['to-be-number'] = _partial
	formatters['to-be-table'] = _partial
	formatters['to-be-false'] = _partial
	formatters['to-be-true'] = _partial
	formatters['to-be-match'] = _partial
	formatters['to-be-ustring-match'] = _partial
	formatters['to-be-contains'] = _partial
	formatters['to-be-close-to'] = _partial
	function self.format()
		local div = mw.html.create( 'div' )
		if tbl[1] then
			div:node( formatters[tbl[1]]( unpack( tbl ) ) )
		end
		local rep = (stats and stats.numfailed > 0) and '[[Category:Failed bdd test sets]]' or ''
		return div:allDone() -- .. ' ' .. rep
	end

	return self
end
			
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
	
	-- constructor for expectations
	local function Expect( str, actual )
		
		local self = {}
		
		-- @var list of processes to run before matching
		self.preprocess = {}
		
		-- @var similarity process
		self.similarity = {}
		
		-- @var list of processes to run after matching
		self.postprocess = {}
		
		local function exec( ... )
			local values = {}
			-- this is the actual argument
			values[1+#values] = { 'actual', 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
			--local last = values[#values]
			--local final = last[#last]
		--	if final then
		--		accs[#accs][2] = 1+accs[#accs][2]
		--	else
		--		accs[#accs][3] = 1+accs[#accs][3]
		--	end
			return values
		end
		
		do
			local preprocess = {}
			preprocess['asUpper'] = function ( str ) return str:upper() end
			preprocess['asLower'] = function ( str ) return str:lower() end
			preprocess['asUpperFirst'] = function ( str ) return str:sub(1,1):upper()..str:sub(2) end
			preprocess['asLowerFirst'] = function ( str ) return str:sub(1,1):lower()..str:sub(2) end
			preprocess['asIdentity'] = function ( str ) return str end
			for k,v in pairs( preprocess ) do
				self[k] = function()
					self.preprocess[1+#self.preprocess] = { buildName(k), v }
					return self
				end
			end
			self.upper = self.asUpper
			self.asUC = self.asUpper
			self.uc = self.asUpper
			self.lower = self.asLower
			self.asLC = self.asLower
			self.lc = self.asLower
			self.upperfirst = self.asUpperFirst
			self.asUCFirst = self.asUpperFirst
			self.asUcFirst = self.asUpperFirst
			self.asUCfirst = self.asUpperFirst
			self.ucfirst = self.asUpperFirst
			self.lowerfirst = self.asLowerFirst
			self.asLCFirst = self.asLowerFirst
			self.asLcFirst = self.asLowerFirst
			self.asLCfirst = self.asLowerFirst
			self.lcfirst = self.asLowerFirst
		end
	
		do
			local postprocess = {}
			postprocess['whenInvert'] = function ( bool ) return not bool end
			postprocess['whenIdentity'] = function ( bool ) return bool end
			for k,v in pairs( postprocess ) do
				self[k] = function()
					self.postprocess[1+#self.postprocess] = { buildName(k), v }
					return self
				end
			end
			self.invert = self.whenInvert
			self.whenInv = self.whenInvert
			self.inv = self.whenInvert
		end
	
		do
			local similarity = {}
			similarity['ifBoolean'] = function ( a ) return type( a ) == 'boolean' end
			similarity['ifString'] = function ( a ) return type( a ) == 'string' end
			similarity['ifNumber'] = function ( a ) return type( a ) == 'number' end
			similarity['ifTable'] = function ( a ) return type( a ) == 'table' end
			similarity['ifNil'] = function ( a ) return type( a ) == 'nil' end
			similarity['ifFail'] = function ( a, b ) return false end
			similarity['ifEqual'] = function ( a, b ) return a == b end
			similarity['ifDeepEqual'] = function ( a, b ) return deepEqual( a, b) end
			similarity['ifLesserThan'] = function ( a, b ) return a < b end
			similarity['ifGreaterThan'] = function ( a, b ) return a > b end
			similarity['ifLesserOrEqual'] = function ( a, b ) return a <= b end
			similarity['ifGreaterOrEqual'] = function ( a, b ) return a >= b end
			similarity['ifFalse'] = function ( a ) return type(a) == 'boolean' and a == false end
			similarity['ifTrue'] = function ( a ) return type(a) == 'boolean' and a == true end
			similarity['ifFalsy'] = function ( a ) return not a end
			similarity['ifTruthy'] = function ( a ) return not not a end
			similarity['ifFirstIdentity'] = function ( a, b ) return a end
			similarity['ifSecondIdentity'] = function ( a, b ) return b end
			similarity['ifMatch'] = function ( a, b ) local res = string.match(a, b); return res or false end
			similarity['ifUstringMatch'] = function ( a, b ) local res = mw.ustring.match(a, b);  return res or false end
			similarity['ifContains'] = function ( a, b ) return not not contains( a, b ) end
			similarity['ifCloseTo'] = function ( a, b, c ) return b-c <= a and a <= b+c end
			for k,v in pairs( similarity ) do
				self[k] = function()
					self.similarity = { buildName(k), v }
					return self
				end
			end
			self.equal = self.ifEqual
			self.deepequal = self.ifDeepEqual
			self.ifLesser = self.ifLesserThan
			self.ifLT = self.ifLesserThan
			self.lesser = self.ifLesserThan
			self.ifGreater = self.ifGreaterThan
			self.ifGT = self.ifGreaterThan
			self.greater = self.ifGreaterThan
			self.ifLesserEqual = self.ifLesserOrEqual
			self.ifLE = self.ifLesserOrEqual
			self.lesserequal = self.ifLesserOrEqual
			self.ifGreaterEqual = self.ifGreaterOrEqual
			self.ifGE = self.ifGreaterOrEqual
			self.greaterequal = self.ifGreaterOrEqual
			self.False = self.ifFalse
			self.True = self.ifTrue
			self.Nil = self.ifNil
			self.falsy = self.ifFalsy
			self.truthy = self.ifTruthy
			self.ifContain = self.ifContains
			self.contains = self.ifContains
			self.contain = self.ifContains
			self.closeTo = self.ifCloseTo
			
			-- this is default
			self.similarity = { buildName('ifEqual'), similarity['ifEqual'] }

			local matchers = {}
			matchers['toBe'] = false
			matchers['toBeBoolean'] = similarity['ifBoolean']
			matchers['toBeString'] = similarity['ifString']
			matchers['toBeNumber'] = similarity['ifNumber']
			matchers['toBeTable'] = similarity['ifTable']
			matchers['toBeNil'] = similarity['ifNil']
			matchers['toBeDeepEqual'] = similarity['ifDeepEqual']
			matchers['toBeFailing'] = similarity['ifFail']
			matchers['toBeLesserThan'] = similarity['ifLesserThan']
			matchers['toBeGreaterThan'] = similarity['ifGreaterThan']
			matchers['toBeLesserOrEqual'] = similarity['ifLesserOrEqual']
			matchers['toBeGreaterOrEqual'] = similarity['ifGreaterOrEqual']
			matchers['toBeFalsy'] = similarity['ifFalsy']
			matchers['toBeTruthy'] = similarity['ifTruthy']
			matchers['toBeFalse'] = similarity['ifFalse']
			matchers['toBeTrue'] = similarity['ifTrue']
			matchers['toBeFirstIdentity'] = similarity['ifFirstIdentity']
			matchers['toBeSecondIdentity'] = similarity['ifSecondIdentity']
			matchers['toBeMatch'] = similarity['ifMatch']
			matchers['toBeUstringMatch'] = similarity['ifUstringMatch']
			matchers['toBeContains'] = similarity['ifContains']
			matchers['toBeCloseTo'] = similarity['ifCloseTo']
			for k,v in pairs( matchers ) do
				self[k] = function( ... )
					if v then
						self.similarity = { buildName(k), v }
					end
					local res = exec( ... )
					-- @todo must be wrapped up and made html safe
					-- str comes from the constructor
					local rep = { "expect", 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
			self.be = self.toBe
			self.beFalsy = self.toBeFalsy
			self.beTruthy = self.toBeTruthy
			self.beNil = self.toBeNil
			self.beMatch = self.toBeMatch
			self.toMatch = self.toBeMatch
			self.beEqual = self.toBe
			self.toEqual = self.toBe
			self.beDeepEqual = self.toBeDeepEqual
			self.toDeepEqual = self.toBeDeepEqual
			self.toContains = self.toBeContains
			self.toContain = self.toBeContains
		end
		
		-- match for "close to"
		-- @return table
		--function self.toBeCloseTo()
		--	return self
		--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( 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 = {
		describe = frame('describe'),
		context = frame('context'),
		it = frame('it'),
		test = it,
		pending = xframe('pending'),
		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,
		view = View,
		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