Skip to content
This repository was archived by the owner on Sep 14, 2024. It is now read-only.

Add expect.extend #142

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# TestEZ Changelog

## Unreleased Changes
* Added `expect.extend` which allows projects to register their own, opinionated expectations that integrates into `expect`. ([#142](https://github.com/Roblox/testez/pull/142))
* Modeled after [jest's implementation](https://jestjs.io/docs/en/expect#expectextendmatchers).
* Matchers are functions that should return an object with with two keys, boolean `pass` and a string `message`
* Like `context`, matchers introduced via `expect.extend` will be present on all nodes below the node that introduces the matchers.
* Limitations:
* `expect.extend` cannot be called from within `describe` blocks
* Custom matcher names cannot overwrite pre-existing matchers, including default matchers and matchers introduces from previous `expect.extend` calls.

## 0.3.3 (2020-09-25)
* Remove the lifecycle hooks from the session tree. This prevents the `[?]` spam from the reporter not recognizing these nodes.
Expand Down
40 changes: 37 additions & 3 deletions src/Expectation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ function Expectation.new(value)
local self = {
value = value,
successCondition = true,
condition = false
condition = false,
matchers = {},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is matchers used? I don't see it anywhere else.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use it once when we need to negate our expectation
https://github.com/Roblox/testez/pull/142/files#diff-c51d01b32afee27efe59f26103c1e704R129

We pass in the non-bound versions of the extend config.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yep. There it is.

_boundMatchers = {},
}

setmetatable(self, Expectation)
Expand All @@ -91,6 +93,31 @@ function Expectation.new(value)
return self
end

function Expectation.checkMatcherNameCollisions(name)
if SELF_KEYS[name] or NEGATION_KEYS[name] or Expectation[name] then
return false
end

return true
end

function Expectation:extend(matchers)
self.matchers = matchers or {}

for name, implementation in pairs(self.matchers) do
self._boundMatchers[name] = bindSelf(self, function(_self, ...)
local result = implementation(self.value, ...)
local pass = result.pass == self.successCondition

assertLevel(pass, result.message, 3)
self:_resetModifiers()
return self
end)
end

return self
end

function Expectation.__index(self, key)
-- Keys that don't do anything except improve readability
if SELF_KEYS[key] then
Expand All @@ -99,12 +126,16 @@ function Expectation.__index(self, key)

-- Invert your assertion
if NEGATION_KEYS[key] then
local newExpectation = Expectation.new(self.value)
local newExpectation = Expectation.new(self.value):extend(self.matchers)
newExpectation.successCondition = not self.successCondition

return newExpectation
end

if self._boundMatchers[key] then
return self._boundMatchers[key]
end

-- Fall back to methods provided by Expectation
return Expectation[key]
end
Expand Down Expand Up @@ -151,6 +182,9 @@ function Expectation:a(typeName)
return self
end

-- Make alias public on class
Expectation.an = Expectation.a

--[[
Assert that our expectation value is truthy
]]
Expand Down Expand Up @@ -274,4 +308,4 @@ function Expectation:throw(messageSubstring)
return self
end

return Expectation
return Expectation
38 changes: 38 additions & 0 deletions src/ExpectationContext.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
local Expectation = require(script.Parent.Expectation)
local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions

local function copy(t)
local result = {}

for key, value in pairs(t) do
result[key] = value
end

return result
end

local ExpectationContext = {}
ExpectationContext.__index = ExpectationContext

function ExpectationContext.new(parent)
local self = {
_extensions = parent and copy(parent._extensions) or {},
}

return setmetatable(self, ExpectationContext)
end

function ExpectationContext:startExpectationChain(...)
return Expectation.new(...):extend(self._extensions)
end

function ExpectationContext:extend(config)
for key, value in pairs(config) do
assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key))
assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key))

self._extensions[key] = value
end
end

return ExpectationContext
10 changes: 9 additions & 1 deletion src/TestPlan.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,15 @@ local function newEnvironment(currentNode, extraEnvironment)
env.fdescribe = env.describeFOCUS
env.xdescribe = env.describeSKIP

env.expect = Expectation.new
env.expect = setmetatable({
extend = function(...)
error("Cannot call \"expect.extend\" from within a \"describe\" node.")
end,
}, {
__call = function(_self, ...)
return Expectation.new(...)
end,
})

return env
end
Expand Down
17 changes: 13 additions & 4 deletions src/TestRunner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
state is contained inside a TestSession object.
]]

local Expectation = require(script.Parent.Expectation)
local TestEnum = require(script.Parent.TestEnum)
local TestSession = require(script.Parent.TestSession)
local LifecycleHooks = require(script.Parent.LifecycleHooks)
Expand All @@ -17,8 +16,16 @@ local TestRunner = {
environment = {}
}

function TestRunner.environment.expect(...)
return Expectation.new(...)
local function wrapExpectContextWithPublicApi(expectationContext)
return setmetatable({
extend = function(...)
expectationContext:extend(...)
end,
}, {
__call = function(_self, ...)
return expectationContext:startExpectationChain(...)
end,
})
end

--[[
Expand Down Expand Up @@ -70,6 +77,8 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks)
errorMessage = messagePrefix .. message .. "\n" .. debug.traceback()
end

testEnvironment.expect = wrapExpectContextWithPublicApi(session:getExpectationContext())

local context = session:getContext()

local nodeSuccess, nodeResult = xpcall(
Expand Down Expand Up @@ -174,4 +183,4 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks)
lifecycleHooks:popHooks()
end

return TestRunner
return TestRunner
19 changes: 16 additions & 3 deletions src/TestSession.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
local TestEnum = require(script.Parent.TestEnum)
local TestResults = require(script.Parent.TestResults)
local Context = require(script.Parent.Context)
local ExpectationContext = require(script.Parent.ExpectationContext)

local TestSession = {}

Expand All @@ -25,6 +26,7 @@ function TestSession.new(plan)
results = TestResults.new(plan),
nodeStack = {},
contextStack = {},
expectationContextStack = {},
hasFocusNodes = false
}

Expand Down Expand Up @@ -98,12 +100,16 @@ end
function TestSession:pushNode(planNode)
local node = TestResults.createNode(planNode)
local lastNode = self.nodeStack[#self.nodeStack] or self.results
local lastContext = self.contextStack[#self.contextStack]
local context = Context.new(lastContext)

table.insert(lastNode.children, node)
table.insert(self.nodeStack, node)

local lastContext = self.contextStack[#self.contextStack]
local context = Context.new(lastContext)
table.insert(self.contextStack, context)

local lastExpectationContext = self.expectationContextStack[#self.expectationContextStack]
local expectationContext = ExpectationContext.new(lastExpectationContext)
table.insert(self.expectationContextStack, expectationContext)
end

--[[
Expand All @@ -113,6 +119,7 @@ function TestSession:popNode()
assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
table.remove(self.nodeStack, #self.nodeStack)
table.remove(self.contextStack, #self.contextStack)
table.remove(self.expectationContextStack, #self.expectationContextStack)
end

--[[
Expand All @@ -123,6 +130,12 @@ function TestSession:getContext()
return self.contextStack[#self.contextStack]
end


function TestSession:getExpectationContext()
assert(#self.expectationContextStack > 0, "Tried to get expectationContext from an empty stack!")
return self.expectationContextStack[#self.expectationContextStack]
end

--[[
Tells whether the current test we're in should be skipped.
]]
Expand Down
25 changes: 25 additions & 0 deletions tests/failing/expectExtend-context-duplicate.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- luacheck: globals describe beforeAll expect

local noOptMatcher = function(_received, _expected)
return {
message = "",
pass = true,
}
end

return function()
beforeAll(function()
expect.extend({
customMatcher = noOptMatcher,
})
end)

describe("redefine matcher", function()
beforeAll(function()
expect.extend({
-- This should throw since we are redefining the same matcher
customMatcher = noOptMatcher,
})
end)
end)
end
16 changes: 16 additions & 0 deletions tests/failing/expectExtend-describe.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- luacheck: globals describe expect

local noOptMatcher = function(_received, _expected)
return {
message = "",
pass = true,
}
end

return function()
describe("SHOULD NOT work in a describe block", function()
expect.extend({
test = noOptMatcher,
})
end)
end
64 changes: 64 additions & 0 deletions tests/passing/expectExtend-context.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- luacheck: globals describe beforeAll expect it

local noOptMatcher = function(_received, _expected)
return {
message = "",
pass = true,
}
end

return function()
beforeAll(function()
expect.extend({
scope_0 = noOptMatcher,
})
end)

it("SHOULD inherit from previous beforeAll", function()
assert(expect().scope_0, "should have scope_0")
end)

describe("scope 1", function()
beforeAll(function()
expect.extend({
scope_1 = noOptMatcher,
})
end)

it("SHOULD inherit from previous beforeAll", function()
assert(expect().scope_1, "should have scope_1")
end)

it("SHOULD inherit from previous root level beforeAll", function()
assert(expect().scope_0, "should have scope_0")
end)

it("SHOULD NOT inherit scope 2", function()
assert(expect().scope_2 == nil, "should not have scope_0")
end)

describe("scope 2", function()
beforeAll(function()
expect.extend({
scope_2 = noOptMatcher,
})
end)

it("SHOULD inherit from previous beforeAll in scope 2", function()
assert(expect().scope_2, "should have scope_2")
end)

it("SHOULD inherit from previous beforeAll in scope 1", function()
assert(expect().scope_1, "should have scope_1")
end)

it("SHOULD inherit from previous beforeAll in scope 0", function()
assert(expect().scope_0, "should have scope_0")
end)
end)
end)

it("SHOULD NOT inherit from scope 1", function()
assert(expect().scope_1 == nil, "should not have scope_1")
end)
end
Loading