Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async support (🚧WIP) #13

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
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
209 changes: 110 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,118 +112,129 @@ See [the Jestronaut Garry's Mod addon README](./gmod-addon/README.md) for more i
Jestronaut tries to match [the API of Jest](https://jestjs.io/docs/api) as closely as possible. Here are some examples of how to use it:

```lua
describe("test organization into suites", function()
it("should confirm basic math", function()
expect(1 + 1):toBe(2)
describe('readme examples', function()
local ranBeforeAll = 0
beforeAll(function()
ranBeforeAll = ranBeforeAll + 1
end)
end)

it("should let sums NOT match", function()
expect(1 + 1)['not']:toBe(3)
expect(1 + 5)['not']:toBeGreaterThan(7)
end)
describe("test organization into suites", function()
it("should confirm basic math", function()
expect(1 + 1):toBe(2)
end)
end)

it("should have all the matchers Jest has", function()
expect(1 + 1):toBe(2)
expect(0.1 + 5.2):toBeCloseTo(5.3)
expect({}):toBeDefined()
expect(nil):toBeFalsy()
expect(1 + 1):toBeGreaterThan(1)
expect(1 + 1):toBeGreaterThanOrEqual(2)
expect(1 + 1):toBeLessThan(3)
expect(1 + 1):toBeLessThanOrEqual(2)
expect(0/0):toBeNaN()
expect(nil):toBeNil()
expect(nil):toBeNull()
expect(1 + 1):toBeTruthy()
expect(1 + 1):toBeType('number')
expect(nil):toBeUndefined()
expect({1, 2, 3}):toContain(2)
expect({1, 2, 3}):toContainEqual(2)
expect({1, 2, 3}):toEqual({1, 2, 3})
expect({1, 2, 3}):toHaveLength(3)
expect({
a = 1,
b = 2,
c = 3
}):toHaveProperty('a')
expect("abc"):toMatch("c$") -- Lua patterns
expect({
a = 1,
b = 2,
c = 3
}):toMatchObject({
a = 1,
b = 2
})
expect({}):toStrictEqual({})
expect(function() error('test') end):toThrow('test')
expect(function() error('testing') end):toThrowError('testing')
end)
it("should let sums NOT match", function()
expect(1 + 1)['not']:toBe(3)
expect(1 + 5)['not']:toBeGreaterThan(7)
end)

it('should be able to mock function implementations', function()
local mockFn = jestronaut:fn(function() return 'x', 'y', 'z' end)
mockFn(1, 2, 3)

expect(mockFn):toHaveBeenCalled()
expect(mockFn):toHaveBeenCalledTimes(1)
expect(mockFn):toHaveBeenCalledWith(1, 2, 3)

mockFn(3, 2, 1)
expect(mockFn):toHaveBeenLastCalledWith(3, 2, 1)
expect(mockFn):toHaveBeenNthCalledWith(1, 1, 2, 3)
expect(mockFn):toHaveLastReturnedWith('x', 'y', 'z')
expect(mockFn):toHaveNthReturnedWith(1, 'x', 'y', 'z')
expect(mockFn):toHaveReturned()
expect(mockFn):toHaveReturnedTimes(2)
expect(mockFn):toHaveReturnedWith('x', 'y', 'z')
end)
it("should have all the matchers Jest has", function()
expect(1 + 1):toBe(2)
expect(0.1 + 5.2):toBeCloseTo(5.3)
expect({}):toBeDefined()
expect(nil):toBeFalsy()
expect(1 + 1):toBeGreaterThan(1)
expect(1 + 1):toBeGreaterThanOrEqual(2)
expect(1 + 1):toBeLessThan(3)
expect(1 + 1):toBeLessThanOrEqual(2)
expect(0 / 0):toBeNaN()
expect(nil):toBeNil()
expect(nil):toBeNull()
expect(1 + 1):toBeTruthy()
expect(1 + 1):toBeType('number')
expect(nil):toBeUndefined()
expect({ 1, 2, 3 }):toContain(2)
expect({ 1, 2, 3 }):toContainEqual(2)
expect({ 1, 2, 3 }):toEqual({ 1, 2, 3 })
expect({ 1, 2, 3 }):toHaveLength(3)
expect({
a = 1,
b = 2,
c = 3
}):toHaveProperty('a')
expect("abc"):toMatch("c$") -- Lua patterns
expect({
a = 1,
b = 2,
c = 3
}):toMatchObject({
a = 1,
b = 2
})
expect({}):toStrictEqual({})
expect(function() error('test') end):toThrow('test')
expect(function() error('testing') end):toThrowError('testing')
end)

local ranBefore = 0
beforeAll(function()
ranBefore = ranBefore + 1
end)
it('should be able to mock function implementations', function()
local mockFn = jestronaut:fn(function() return 'x', 'y', 'z' end)
mockFn(1, 2, 3)

expect(mockFn):toHaveBeenCalled()
expect(mockFn):toHaveBeenCalledTimes(1)
expect(mockFn):toHaveBeenCalledWith(1, 2, 3)

mockFn(3, 2, 1)
expect(mockFn):toHaveBeenLastCalledWith(3, 2, 1)
expect(mockFn):toHaveBeenNthCalledWith(1, 1, 2, 3)
expect(mockFn):toHaveLastReturnedWith('x', 'y', 'z')
expect(mockFn):toHaveNthReturnedWith(1, 'x', 'y', 'z')
expect(mockFn):toHaveReturned()
expect(mockFn):toHaveReturnedTimes(2)
expect(mockFn):toHaveReturnedWith('x', 'y', 'z')
end)

it('should run beforeAll for each "it" this checks how many (so far)', function()
expect(ranBefore):toEqual(5)
end)
local ranBefore = 0
beforeEach(function()
ranBefore = ranBefore + 1
end)

it('can spy on a property setter', function()
local audio = {
volume = 0,
}
local spy = jestronaut:spyOn(audio, 'volume', 'set')
audio.volume = 100
it('should run beforeEach for each "it" this checks how many (so far)', function()
expect(ranBefore):toEqual(5) -- 5 because this is the 5th "it" in this file
end)

expect(spy):toHaveBeenCalled()
expect(audio.volume):toBe(100)
end)
it('should run beforeAll only once', function()
expect(ranBeforeAll):toEqual(1)
end)

it('can spy on a property getter', function()
local audio = {
volume = 0,
}
local spy = jestronaut:spyOn(audio, 'volume', 'get')
print(audio.volume)
it('can spy on a property setter', function()
local audio = {
volume = 0,
}
local spy = jestronaut:spyOn(audio, 'volume', 'set')
audio.volume = 100

expect(spy):toHaveBeenCalled()
expect(audio.volume):toBe(0)
end)
expect(spy):toHaveBeenCalled()
expect(audio.volume):toBe(100)
end)

it:failing('should be able to expected failures', function()
expect(1 + 1):toEqual(1)
end)
it('can spy on a property getter', function()
local audio = {
volume = 0,
}
local spy = jestronaut:spyOn(audio, 'volume', 'get')
print(audio.volume)

it:skip('should be able to skip tests', function()
expect(1 + 1):toEqual(1)
end)
expect(spy):toHaveBeenCalled()
expect(audio.volume):toBe(0)
end)

it:failing('should be able to expected failures', function()
expect(1 + 1):toEqual(1)
end)

it:each({{1, 1, 2}, {1, 2, 3}, {2, 1, 3}})(
"can loop data (%i, %i = %i)",
function(a, b, expected)
expect(a + b):toBe(expected)
end
)
it:skip('should be able to skip tests', function()
expect(1 + 1):toEqual(1)
end)

it:each({ { 1, 1, 2 }, { 1, 2, 3 }, { 2, 1, 3 } })(
"can loop data (%i, %i = %i)",
function(a, b, expected)
expect(a + b):toBe(expected)
end
)
end)
```

Check out [the Jestronaut tests](https://github.com/luttje/jestronaut/tree/main/tests) for more examples.
Expand Down
32 changes: 16 additions & 16 deletions gmod-addon/lua/sh_jestronaut_gmod_reporter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,20 @@ local function drawDescribeOrTest(describeOrTest)
summary:plain(" " .. describeOrTest.name .. "\n")

if (describeOrTest.hasRun and not describeOrTest.success) then
summary:plain(table.concat(describeOrTest.errors) .. "\n\n")
summary:plain(describeOrTest.errorMessage .. "\n\n")
end
end
end
elseif (describeOrTest.hasRun and not describeOrTest.success) then
summary:plain(table.concat(describeOrTest.errors) .. "\n\n")
summary:plain(describeOrTest.errorMessage .. "\n\n")
end

return tostring(summary)
end

--- Prints the name of the test.
--- @param describeOrTest DescribeOrTest
function REPORTER:testStarting(describeOrTest)
function REPORTER:onTestStarting(describeOrTest)
-- Override print so there's no interference with the test output.
print = function() end -- TODO: Store the print and output it at the end of the test.

Expand All @@ -112,12 +112,10 @@ function REPORTER:testStarting(describeOrTest)
originalPrint(ensureLength("STARTED:", 10) .. tostring(summary))
end

--- Prints the result of the test and returns whether it passed.
--- Prints the result of the test.
--- @param describeOrTest DescribeOrTest
--- @param success boolean
--- @param ... any
--- @return boolean
function REPORTER:testFinished(describeOrTest, success, ...)
function REPORTER:onTestFinished(describeOrTest, success)
print = originalPrint

local file = self:getFileByPath(describeOrTest.filePath)
Expand Down Expand Up @@ -149,7 +147,7 @@ end

--- Prints the skip message of the test.
--- @param describeOrTest DescribeOrTest
function REPORTER:testSkipped(describeOrTest)
function REPORTER:onTestSkipped(describeOrTest)
local file = self:getFileByPath(describeOrTest.filePath)

if file then
Expand All @@ -165,7 +163,7 @@ end
--- Prints the retry message of the test.
--- @param describeOrTest DescribeOrTest
--- @param retryCount number
function REPORTER:testRetrying(describeOrTest, retryCount)
function REPORTER:onTestRetrying(describeOrTest, retryCount)
self:redrawSummary(self.isVerbose)
end

Expand Down Expand Up @@ -200,8 +198,9 @@ end
--- Stores the tests that will be run and prints the summary with header.
--- @param rootDescribe Describe
--- @param describesByFilePath table
function REPORTER:startTestSet(rootDescribe, describesByFilePath)
local totalTestCount = rootDescribe.childCount + rootDescribe.grandChildrenCount
--- @param skippedTestCount number
function REPORTER:onStartTestSet(rootDescribe, describesByFilePath, skippedTestCount)
local totalTestCount = rootDescribe.childTestCount + rootDescribe.grandChildrenTestCount + skippedTestCount

self.summaryHeader = styledText.new(nil, STYLING_DISABLED)
:plain("🚀 Starting ")
Expand All @@ -216,12 +215,13 @@ function REPORTER:startTestSet(rootDescribe, describesByFilePath)
end

--- Prints the success message of the test.
--- @param rootDescribe Describe
--- @param processedTests DescribeOrTest[]
--- @param passedTestCount number
--- @param failedTestCount number
--- @param skippedTestCount number
--- @param duration number
function REPORTER:printEnd(rootDescribe, failedTestCount, skippedTestCount, duration)
local totalTestCount = rootDescribe.childCount + rootDescribe.grandChildrenCount
--- @param testDuration number
function REPORTER:onEndTestSet(processedTests, passedTestCount, failedTestCount, skippedTestCount, testDuration)
local totalTestCount = passedTestCount + failedTestCount + skippedTestCount
local notRunCount = failedTestCount + skippedTestCount
local relativeSuccess = 1 - (notRunCount / totalTestCount)

Expand Down Expand Up @@ -261,7 +261,7 @@ function REPORTER:printEnd(rootDescribe, failedTestCount, skippedTestCount, dura

originalPrint(
styledText.new(nil, STYLING_DISABLED)
:plain("Time: " .. duration .. "s")
:plain("Time: " .. testDuration .. "s")
)

self:printNewline()
Expand Down
11 changes: 11 additions & 0 deletions gmod-addon/lua/sh_jestronaut_gmod_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@
jestronaut
:configure({
roots = {
"addons/jestronaut/lua/",
"addons/jestronaut/lua/tests/",
"addons/jestronaut/lua/tests/generated/",
},

-- Sets up the event loop ticker for Garry's Mod
eventLoopTicker = function(ticker)
hook.Add("Think", "AsyncTestRunner", function()
if ticker() == false then
hook.Remove("Think", "AsyncTestRunner")
end
end)
end,

reporter = GmodReporter
})
:registerTests(function()
jestronaut.callWithRequireCompat(function()
-- require "tests/generated/init" -- TODO: fails atm
include("sh_jestronaut_test_mysqloo.lua")
require "tests/init"
end)
end)
Expand Down
15 changes: 15 additions & 0 deletions gmod-addon/lua/sh_jestronaut_test_mysqloo.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
describe('MySQLOO async tests', function()
itAsync('should connect to the database', function(done)
print('Connecting to the database')
local db = mysqloo.connect('127.0.0.1', 'root', '', 'blackbox_experiment', 3306)

db.onConnected = function()
print('Connected to the database')
expect(db:status()):toBe(mysqloo.DATABASE_CONNECTED)
db:disconnect()
done()
end

db:connect()
end)
end)
11 changes: 3 additions & 8 deletions libs/jestronaut.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ local JESTRONAUT = {
function JESTRONAUT:configure(runnerOptions)
environmentLib.resetEnvironment()

-- Setup the root describe
describeLib.describe("root", function() end)
-- Setup the root describe where every other describe and test will be nested under
describeLib.describeTransparent("root", function() end)

runnerOptions = optionsLib.merge(runnerOptions)

Expand Down Expand Up @@ -54,19 +54,14 @@ function JESTRONAUT:registerTests(testRegistrar)
end

--- Runs the tests.
--- @param onFinishedCallback? fun()
--- @return Jestronaut
function JESTRONAUT:runTests(onFinishedCallback)
function JESTRONAUT:runTests()
if not self.runnerOptions then
error("No options found. You must setup jestronaut (with jestronaut:configure(options)) before running tests.")
end

environmentLib.runTests(self.runnerOptions)

if onFinishedCallback then
onFinishedCallback()
end

return self
end

Expand Down
Loading
Loading