views:

471

answers:

1

I'm a Lua novice. I'm unit testing Lua 5.1 code using Lunity and LeMock.

My class is StorageManager. I'm unit testing its load() method, which loads files from disk. I don't want my unit tests dependent on actual files on the actual disk to test that.

So, I'm trying to inject the I/O dependency with a mock object and verify that object's behavior during my test. I can't figure out how to use the I/O object with a syntax that works when called by both my mock-based unit test and the "real code".

How can I change the code (the load() method, preferably) so that it will do its job when called from either unit test (the one without the mock is temporary until I get this figured out -- it resembles the code that will later actually call the method under test)?

Note1: If you run these tests, remember that the "without mock" test expects a file on disk whose filename matches VALID_FILENAME.

Note2: I thought about using pcall's try/catch-like behavior to execute one line or the other (see storageManager.lua lines 11 & 12). Assuming that's even possible, it feels like a hack, and it traps errors I might later want thrown (out of load()). Please expound on this option if you see no alternative.

test_storageManager.lua:

 1 require "StorageManager"
 2 require "lunity"
 3 require "lemock"
 4 module("storageManager", package.seeall, lunity)
 5 
 6 VALID_FILENAME = "storageManagerTest.dat"
 7 
 8 function setup()
 9     mc = lemock.controller()
10 end
11 
12 function test_load_reads_file_properly()
13     io_mock = mc:mock()
14     file_handle_mock = mc:mock()
15     io_mock:open(VALID_FILENAME, "r");mc:returns(file_handle_mock)
16     file_handle_mock:read("*all")
17     file_handle_mock:close()
18     mc:replay()
19     storageManager = StorageManager:new{ io = io_mock }
20     storageManager:load(VALID_FILENAME)
21     mc:verify()
22 end
23 
24 function test_load_reads_file_properly_without_mock()
25     storageManager = StorageManager:new()
26     storageManager:load(VALID_FILENAME)
27 end
28 
29 runTests{useANSI = false}

storageManager.lua:

 1 StorageManager = {}
 2 
 3 function StorageManager.new (self,init)
 4     init = init or { io=io } -- I/O dependency injection attempt
 5     setmetatable(init,self)
 6     self.__index = self
 7     return init
 8 end
 9 
10 function StorageManager:load(filename)
11     file_handle = self['io'].open(self['io'], filename, "r") -- works w/ mock
12     -- file_handle = io.open(filename, "r") -- works w/o mock
13     result = file_handle:read("*all")
14     file_handle:close()
15     return result
16 end

Edit:

These classes pass both tests. Many thanks to the RBerteig.

test_storageManager.lua

 1 require "admin.StorageManager"
 2 require "tests.lunity"
 3 require "lib.lemock"
 4 module("storageManager", package.seeall, lunity)
 5 
 6 VALID_FILENAME = "storageManagerTest.dat"
 7 
 8 function setup()
 9     mc = lemock.controller()
10 end
11
12 function test_load_reads_file_properly()
13     io_mock = mc:mock()
14     file_handle_mock = mc:mock()
15     io_mock.open(VALID_FILENAME, "r");mc:returns(file_handle_mock)
16     file_handle_mock:read("*all")
17     file_handle_mock:close()
18     mc:replay()
19     local saved_io = _G.io
20     _G.io = io_mock
21     package.loaded.io = io_mock
22     storageManager = StorageManager:new()
23     storageManager:load(VALID_FILENAME)
24     _G.io = saved_io
25     package.loaded.io = saved_io
26     mc:verify()
27 end
28
29 function test_load_reads_file_properly_without_mock()
30     storageManager = StorageManager:new()
31     storageManager:load(VALID_FILENAME)
32 end
33
34 runTests{useANSI = false}

storageManager.lua

 1 StorageManager = {}
 2 
 3 function StorageManager.new (self,init)
 4     init = init or {}
 5     setmetatable(init,self)
 6     self.__index = self
 7     return init
 8 end
 9 
10 function StorageManager:load(filename)
11     file_handle = io.open(filename, "r")
12     result = file_handle:read("*all")
13     file_handle:close()
14     return result
15 end
+1  A: 

I think that you are making the problem more difficult than it has to be.

Assuming that the module storageManager.lua is not itself localizing the io module, then all you need to do is to replace the global io with your mock object while running the test.

If the module does localize the io object for performance, then you would need to inject the new value of io before loading the module. This might mean that you need to make the call to require part of the test case setup (and a matching cleanup that removes all traces of the module from package.loaded and _G) so that it can be mocked differently in different test cases. WinImage

Edit:

By localizing a module for performance I mean the Lua idiom of copying the module's methods into local variables in the module's name space. For example:

-- somemodule.lua
require "io"
require "math"

-- copy io and math to local variables
local io,math=io,math

-- begin the module itself, note that package.seeall is not used so globals are
-- not visible after this point
module(...)

function doMathAndIo()
    -- does something interesting here
end

If you do this, the references to the stock modules io and math are made at the moment that require "somemodule" is executed. Replacing either of those modules after the call to require() with a mocked version will not be effective.

To effectively mock a module that is used with this idiom, you would have to have the mock object in place before the call to require().

Here is a how I would go about replacing the io object for the duration of the call in a test case:


function test_load_reads_file_properly()
    io_mock = mc:mock()
    file_handle_mock = mc:mock()
    io_mock:open(VALID_FILENAME, "r");mc:returns(file_handle_mock)
    file_handle_mock:read("*all")
    file_handle_mock:close()
    mc:replay()

    local saved_io = _G.io
    _G.io = io_mock
    package.loaded.io = io_mock
    storageManager = StorageManager:new{ }
    storageManager:load(VALID_FILENAME)
    _G.io = saved_io
    package.loaded.io = saved_io
    mc:verify()
end

I may not be restoring the real object at exactly the right moment, and this is untested, but it should point you in the right direction.

RBerteig
I don't mean (or know how) to "localize the io object for performance", but I do want to "replace the global io with [my] mock object while running the test". I thought I was doing that on storageManager.lua:4? How does one properly "replace the global io"? Can you provide (or point me to) example code?
lance
Your edit gave me what I needed. I've edited my changed code into the original question. Many thanks.
lance