lunarmodules / busted

Elegant Lua unit testing.
https://lunarmodules.github.io/busted/
MIT License
1.38k stars 184 forks source link

mock other libraries (local scope) #572

Open citation opened 6 years ago

citation commented 6 years ago

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!

Tieske commented 6 years ago

Have you looked at spies, stubs and mocks? Also luassert snapshots might be of help.

mikz commented 6 years ago
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.

mkpankov commented 5 years ago

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'}}
Tieske commented 5 years ago

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.

sandervanburken commented 4 years ago

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?

Mock selected functions from a dependent library

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)
Tieske commented 4 years ago

@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)
tim-antkowiak commented 4 years ago

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?

Tieske commented 4 years ago

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.

tim-antkowiak commented 4 years ago

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.