I've recently tried the pattern of using `patchFile` for mocking in unit tests
and it worked really well for my use case.
The pattern is in a unit test folder you provide a `config.nims` file using
`patchFile` to override certain modules. Note this will override these modules
for all tests in this folder (AFAICT).
This unit testing is for a pure-Nim simple object store called
`nvs_config_obj.nim` which itself uses a Nim-ified wrapper `nvs.nim` that
provides Non-volatile Storage (NVS) on Zephyr RTOS. I wanted to test both
`nvs_config_obj` and `nvs` modules from the C API layer up and decided that was
the best layer to mock. There are two modules that provide the C FFI for the
low level C Apis from the Zephyr RTOS (`znvs.nim` and `zflash.nim`).
Here's how it worked out. First the `config.nims` setup to override the `znvs`
and `zflash` modules:
switch("path", "$projectDir/../../src")
patchFile("nephyr", "znvs", "znvs_mock")
patchFile("nephyr", "zflash", "zflash_mock")
Run
Next in `znvs_mock.nim` in the same test folder you can override any api's
you're testing. In this case some C read/write APIs using a Table with some
byte arrays was sufficient:
# ...
type
nvs_fs* = object
# mock the C struct `nvs_fs` with our custom one
data*: Table[uint16, array[128, byte]]
proc nvs_write*(fs: ptr nvs_fs; id: uint16; data: pointer; len: cint): cint
=
# mock nvs c write function, only up to 128 bytes
var buf: array[128, byte]
copyMem(buf[0].addr, len.unsafeAddr, 1)
copyMem(buf[1].addr, data, len)
fs.data[id] = buf
proc nvs_read*(fs: ptr nvs_fs; id: uint16; data: pointer; len: cint): cint =
# mock nvs c read function, only up to 128 bytes
var buf = fs.data[id]
copyMem(result.addr, buf[0].addr, 1)
copyMem(data, buf[1].addr, len)
Run
Now my unit tests are testing the main `nvs.nim` which wraps some Zephyr RTOS
bits, but I just call and use the `nvs.nim` module as normal in the unit tests:
include nephyr/extras/nvs_config_obj # do an include to access any private
procs/vars
suite "nvs basic config object":
test "basic store":
var nvs = initNvsMock[NvsConfig]()
var settings = newConfigSettings(nvs, ExampleConfigs())
settings.values.dac_calib_gain = 1111
settings.values.dac_calib_offset = 2222
var fld3Val = nvs.read(fld3, float32)
# etc...
Run
There's some modifications to how you import modules in `nvs.nim` to help make
it easier to "intercept" certain modules. In this case it let's me override the
C header imports it tries to do otherwise which is also nice.
I found a bit of trickiness with the raw NVS constructor vs the high level Nim
NVS module. I resolved this by passing in the high-level NVS object as a
generic type. This allowed me to mock only the lower level `znvs.nim` module
but unit test the higher level NVS wrapper.
proc initNvsMock*[T](): T =
result = T()
discard nvs_init(result.fs.addr, "flash_mock")
Run
You can find the
[config.nims](https://github.com/EmbeddedNim/nephyr/blob/e666520104b45677be3e55ccebd16d0967a88cb3/tests/unit_tests/config.nims#L4)
and the [mocked zephyr rtos nvs api's
here](https://github.com/EmbeddedNim/nephyr/blob/e666520104b45677be3e55ccebd16d0967a88cb3/tests/unit_tests/znvs_mock.nim#L35)
. The actual [unit
tests](https://github.com/EmbeddedNim/nephyr/blob/e666520104b45677be3e55ccebd16d0967a88cb3/tests/unit_tests/t_nvs_cfg_obj.nim#L35).