4

I have a function that calls isFile (from std.file) on a filename and then proceeds appending .1, .2, .3 etc, checking whether each one of those is present.

I want to unit test the function, but to do so I need to mock isFile.

I looked around a bit and I found ways to mock classes but not single functions.

garph0
  • 1,700
  • 1
  • 13
  • 16
  • 1
    one option might be to change the import based on version(unittest) or something... so the real one uses std.file.isFile but the unittest one uses your.mocks.isFile – Adam D. Ruppe Oct 15 '14 at 04:15
  • thanks for the idea, but if I go down this road I'd prefer to wrap the used std functions in some object and use a factory to decide whether to instantiate the real object or the mocks. I'm still looking for a more 'surgical' solution :) – garph0 Oct 15 '14 at 05:39
  • 1
    Sorry, what's wrong with the local import, again? Like, import std.file; import std.stdio; int main(string[] args) { writeln(isFile("qq.d")); return 0; } unittest { import mocks; writeln(isFile("qq.d")); } – Sergei Nosov Oct 15 '14 at 09:08
  • Sergei you're right: I didn't think it through and was going to complicate my life. Adam please write it as an answer so i can accept it. – garph0 Oct 15 '14 at 13:00
  • 1
    Well, specifically, the imports would have to change - a local one I don't think overrides a global one, it would still ask you to disambiguate. So what I'm thinking is versioning the import itself, so only one of them is actually done - no ambiguity then. I gotta run in a minute though, I'll write up the answer when I'm back. – Adam D. Ruppe Oct 15 '14 at 16:27
  • 1
    Actually, the local import does overwrite the global one. See the "Scoped modules" section in http://dlang.org/module.html Quote: "The imports are looked up to satisfy any unresolved symbols at that scope. Imported symbols may hide symbols from outer scopes." – Sergei Nosov Oct 15 '14 at 19:45

2 Answers2

4

Since my answer is slightly different from Adam's, I will add it, and he can add his.

You can use "Scoped imports" for that purpose. See the respective section in the documentation http://dlang.org/module.html

Here's also a working example, how you can mock an isFile function inside a unittest block (assuming it is defined in module "mocks")

import std.file; 
import std.stdio;

int main(string[] args) 
{ 
    writeln(isFile("qq.d")); 
    return 0; 
} 

unittest 
{ 
    import mocks;
    writeln(isFile("qq.d")); 
}
Sergei Nosov
  • 1,637
  • 11
  • 9
4

My simple solution is to mock the functions in a separate module, then use version(unittest) to choose which one you want:

version(unittest)
   import mocks.file;
else
   import std.file

void main() { isFile("foo"); } // std.file normally, mocks.file in test mode

The local import Sergei Nosov works in some cases, but I think the top level one is better because typically you'd want to test your own function:

string test_me() { isFile("qq.d"); return "do something"; }
unittest {
    assert(test_me() == "do something");
}

In that case, the scoped import wouldn't work because isFile is used too far away from the test. The version(unittest) on the import at the usage point, however, could redefine the function as needed.

Perhaps the best would be a combination:

string test_me() {
    version(unittest) bool isFile(string) { return true; }
    else import std.file : isFile;
    isFile("qq.d"); return "do something";
 }

That is, defining the fake function locally... but I don't like that either, now that I think of it, since the function doesn't necessarily know how it will be tested. Maybe the mocks module that is imported actually makes function pointers or something that can be reassigned in the unittest block.... hmm, it may need to be a full blown library, not just a collection of functions.

But I think between our two answers, there's a potential solution.


Third thing I want to mention, though it is kinda insane, is that it is possible to globally replace a function in another module by using some linker tricks:

import std.file;
import std.stdio;

// our replacement for isFile...
pragma(mangle, std.file.isFile.mangleof)
static bool isFile(string) { return true; }

int main(string[] args)
{
    writeln(isFile("qq.d")); // always does true
    return 0;
}

The reason that works is pragma(mangle) changes the name the linker sees. If the linker sees two functions with the same name, one in a library and one in the user code, it allows the user code to replace the individual library function.

Thus, our function is used instead of the lib. Important notes with this: the function signatures must match, or else it will crash when you run it, and it replaces the function for your entire program, not just one location. Could be used with version(unittest) though.

I don't recommend actually using this trick, it is prone to random crashes if you make a mistake, just wanted to throw it out there while thinking about replacing std lib functions.

Perhaps this trick plus function pointers could be used to replace a function at run time. Major problem with that though: since the linker completely replaces the library function with your function, you can't actually use the original implementation at all!

You could also replace a whole std lib module by writing your own, giving it the same name, and passing it to the compiler explicitly. I sometimes do that while doing development work on Phobos. But since this replaces the whole thing and is a compiler command line difference, it probably isn't helpful for unit tests either.

Adam D. Ruppe
  • 25,382
  • 4
  • 41
  • 60
  • 1
    That's a valuable set of options. One comment, though. Importing modules locally seems like a good practice and, to some extent, it is advertised as a good D practice. So, the function test_me could as well import the module locally and the second solution wouldn't work. And in that case it should probably provide a compile-time parameter or something if it cares to be thoughtful of testers (which it should). – Sergei Nosov Oct 16 '14 at 06:47
  • Yes, indeed. Local imports are great. – Adam D. Ruppe Oct 16 '14 at 14:08