Open citation opened 6 years ago
Have you looked at spies, stubs and mocks? Also luassert
snapshots might be of help.
local state = require("luassert.state")
local function snapshot(context)
state.snapshot()
return nil, true
end
local function revert(context)
state.revert()
return nil, true
end
for _, phase in ipairs({ 'suite', 'file', 'describe'}) do
busted.subscribe({ phase, 'start' }, snapshot)
busted.subscribe({ phase, 'end' }, revert)
end
busted.before_each(snapshot)
busted.after_each(revert)
Will do the trick. Just have in your spec helper file and all stubs should be automatically reverted in between describe / it blocks.
I don't see how previous discussion answers the original question. For me as well as original poster it's not clear how to mock libraries, including the standard one.
Mocking includes not only not calling the original method, but also being able to tell the mock what should it return, and in general, how it should behave.
For example, is this possible in busted
?
local Mock = require 'test.mock.Mock'
os.remove = Mock()
os.remove:whenCalled{with={'example/content'}, thenReturn={true}}
os.remove:whenCalled{with={'example'}, thenReturn={true}}
os.remove:whenCalled{thenReturn={nil, 'No such file.'}}
RemoveRecusive('example')
os.remove:assertAnyCallMatches{arguments={'example/content'}}
the typical approach there would be to use a regular Lua function to mock the original one. Doing it from the before_each
handler, and undoing it in the after_each
for example.
Writing a complex Mock library that would generalize in the way your example displays, would be a nice exercise but not worth the effort imo. It's quicker to write a local, specific, mock than go out and read the docs how to use one like that.
Just my 2cts.
I do not like my solution below; it does work though...
(1) Can someone point me to a better lua-mock-pattern?
(2) I added insulate
. But I still need to explicitly undo my mock. Is there way to let busted do that for me?
The goal is to test mylib
. mylib
has a dependency on pl.path
(from Penlight). This code only mocks isdir
and isfile
from pl.path
.
local mylib = require('mylib') -- Library to test; This library requires 'pl.path'
-- Tests that do not need a mock
-- ...
insulate('mock isdir isfile', function()
local mylib
local pl_path = require('pl.path')
package.loaded['pl.path'] = {}
-- Other functions from 'pl.path' should not be mocked
for k,v in pairs(pl_path) do
package.loaded['pl.path'][k] = v
end
package.loaded['pl.path']['isdir'] = function() return true end
package.loaded['pl.path']['isfile'] = function() return true end
-- Reload mylib so it uses the mocked 'pl.path'
package.loaded['mylib'] = nil
subc = require('mylib')
-- Add describe blocks to test mylib with the mocked pl.path
-- ...
-- Reset mock
package.loaded['pl.path'] = nil
package.loaded['subc'] = nil
require('subc')
end)
@sandervanburken I don't think that is the right approach. Reason being that the code of a describe
block (and hence also insulate
blocks) run upon loading the test files. Whereas the tests themselves are stored, and executed after loading the file.
So a test is "defined" when the file is loaded, but "executed" after the loading complete.
So in you example code the part marked with -- Reset mock
will be executed before the test are actually executed.
A simple and clean way of doing it (others may have other preferences);
local mylib = require('mylib') -- Library to test; This library requires 'pl.path'
-- Tests that do not need a mock
-- ...
describe('mock isdir isfile', function()
local mylib
local pl_path
setup(function()
-- clear cached versions
package.loaded['mylib'] = nil
package.loaded['pl.path'] = nil
-- load pl.path and create mocks
pl_path = require('pl.path')
pl_path.isfile = function() return true end
pl_path.isdir = function() return true end
-- now load library to test
mylib = require('mylib') -- Library to test; This library requires 'pl.path'
end)
teardown(function()
-- clear cached versions
package.loaded['mylib'] = nil
package.loaded['pl.path'] = nil
pl_path = nil
mylib = nil
end)
-- Add describe blocks to test mylib with the mocked pl.path
-- ...
end)
Shorter but with more "magic" by insulate
should be:
insulate('Tests that do not need a mock', function()
local mylib = require('mylib') -- Library to test; This library requires 'pl.path'
-- tests here
end)
insulate('mock isdir isfile', function()
-- clear cached versions, just to be sure (since busted also used penlight)
package.loaded['mylib'] = nil
package.loaded['pl.path'] = nil
-- load pl.path and create mocks
local pl_path = require('pl.path')
pl_path.isfile = function() return true end
pl_path.isdir = function() return true end
-- now load library to test
local mylib = require('mylib') -- Library to test; This library requires 'pl.path'
-- tests with mocks here
end)
Mocking a separate library works quite fine, thanks @Tieske !
I'm wondering if it is possible to mock another function inside the same library. For example: mylib:
local https = require('ssl.https')
local function b()
https.request('https://my.url.com')
end
local function a()
b()
-- do some other stuff
end
return {
a = a,
b = b
}
Writing a test for function b works fine with mocking the ssl library. Now I want to write a test for function a. But it seems to be not possible to mock function b inside a test. What is logical because function a will always use the local function b in the same file instead of the mocked one.
I get it to work with using mock() inside the test:
local mylib = require('mylib')
describe('mylib', function()
describe('function a', function()
it('should call function b without running it', function()
local mock_mylib = mock(mylib, true)
mock_mylib.a()
assert.stub(mock_mylib.b).was_called()
mock.revert(mock_mylib)
end)
end)
end)
So function b do not run. But it seems to be that function b will not even be called because the assert gives me a failure that function b was called 0 times.
Do I miss something or is there a better way to do it?
function b
is a locally scoped function to function a
. If you add it to the module table then you can test it.
local https = require('ssl.https')
local mymod = {}
function mymod.b()
https.request('https://my.url.com')
end
function mymod.a()
mymod.b()
-- do some other stuff
end
return mymod
Now if you mock mymod.b
you can test it.
I was feared that this will be the answer 😅
So I assume there is no way to mock a local scoped function without touching mymod?
Just wondering if there is another way.
I'm wanting to write unit tests for an existing lua file using Busted. I want to mock some of the methods that the file calls are pulled in from other lua-libraries. (ie: require 'resty.http’)
local http = require "resty.http" local client = http.new() local res, err = client:request_uri(...)
How can this be achieved? Thanks!