digitalmars.D - We're long overdue for a "D is awesome" post
- H. S. Teoh (83/83) May 30 2023 So here's one.
- Walter Bright (7/8) May 30 2023 Excellent! The technique you're exhibiting is called "mocking". It works...
- H. S. Teoh (15/23) May 30 2023 Yes, it's an age-old technique since at least the days of Java.
- Salih Dincer (9/56) May 30 2023 I didn't know that `while` is used just like `foreach`. 0k4y, we
- H. S. Teoh (6/15) May 30 2023 Oops, that's supposed to be `foreach`, not `while`. :-/ My bad.
- Steven Schveighoffer (23/53) May 30 2023 All of iopipe is 100% unittestable, because all strings are treated as
- Salih Dincer (14/19) May 31 2023 More than admit it, I want it to look cool and humble at the same
- =?UTF-8?Q?Ali_=c3=87ehreli?= (6/10) May 31 2023 Smart! In the past, I used runtime polymorhism to achieve the same. Oops...
So here's one. Today, I was writing some code that iterates over a data structure and writes output to bunch of different files. It looks something like this: void genSplitHtml(Data data, ...) { auto outputTemplate = File("template.html", "r"); foreach (...) { auto filename = generateFilename(...); auto sink = File(filename, "w").lockingTextWriter; ... while (line; outputTemplate.byLine) { if (line.canFind("MAGIC_TOKEN")) { generateOutput(...); } else { sink.put(line); } } } } Since the whole point of this function was to write output to different files (with automatically determined names), I wanted to write a unittest that tests whether it creates the correct files with the correct contents. But I didn't want unittests to touch the actual filesystem either -- didn't want to have to clean up the mess during development where the code might sometimes break and leave detritus behind in a temporary directory, or interact badly with other unittests running in parallel, etc.. And I didn't want to do a massive code refactoring to make genSplitHtml more unittestable. (Which would have more chances of screwing up and having bugs creep in, which defeats the purpose of this exercise.) So I came up with this solution: 1) Rewrite the function to: void genSplitHtml(File = std.stdio.File)(Data data, ...) { ... } The default parameter binds to the real filesystem by default, so other code that uses this function don't have to change to deal with a new API. Then for the unittest code: 2) Create a fake virtual filesystem in my unittest block: static struct MockFile { static string[string] files; string fname; this(string _fname, string mode) { // ignore `mode` for this test fname = _fname; } // Mock replacement for std.stdio.File.lockingTextWriter auto lockingTextWriter() { return (const(char)[] data) { // simulate writing to a file files[fname] ~= data.dup; }; } void rewind() {} // dummy void close() {} // dummy auto byLine() { // We're mostly writing to files, and only // reading from a specific one. So just fake its // contents here. if (fname != "template.html") return []; else return [ "<html>", "MAGIC_TOKEN", "</html>" ]; } } Then instead of calling genSplitHtml(...), the unittest calls it as genSplitHtml!MockFile(...). This replaces std.stdio.File with MockFile, and thanks to D templates and the range API, the rest of the code just adapted itself automatically to the MockFile fake filesystem. After the function is done, the unittest just has to inspect the contents of MockFile.files to verify that the correct files are there, and that their contents are correct. Took me like 10 minutes to write MockFile and a unittest that checks for correct behaviour using the fake filesystem. MockFile can be expanded in the future to simulate, e.g. a full filesystem, a filesystem that occasionally (or always) fails, or corrupts data, etc.: test cases that would be impractical to test with a real filesystem. Best of all, I get all of this "for free": no existing code has to change except for that single new template parameter to the target function. D not only r0x0rs, D boulders!! T -- Heads I win, tails you lose.
May 30 2023
Excellent! The technique you're exhibiting is called "mocking". It works especially well when a function's inputs and outputs are properly parameterized. When I fixed the D lexer.d to not access any globals, I was able to simplify the unittests for it by mocking up the inputs and outputs. For example, instead of having to use the global "gagging" switch to suppress error messages, I used a mock error handler that just did nothing. On 5/30/2023 2:42 PM, H. S. Teoh wrote:So here's one.
May 30 2023
On Tue, May 30, 2023 at 04:27:05PM -0700, Walter Bright via Digitalmars-d wrote:Excellent! The technique you're exhibiting is called "mocking". It works especially well when a function's inputs and outputs are properly parameterized.Yes, it's an age-old technique since at least the days of Java. What makes it stand out in this particular case, though, is D's ability to let you mock references to global (module-level) symbols with just a 1-line change. To mock the top-level `System` in Java, for example, you have to do some arcane black magic with the help of an external package; it's not something for mere mortals or the faint of heart to do by hand. In D, you add a single template parameter to your function, and you're all set to go. Now *that's* power.When I fixed the D lexer.d to not access any globals, I was able to simplify the unittests for it by mocking up the inputs and outputs. For example, instead of having to use the global "gagging" switch to suppress error messages, I used a mock error handler that just did nothing.Good to hear. Error gagging is the source of some of the most irritating D compiler bugs in the past; the less it gets used, the better IMNSHO. T -- Genius may have its limitations, but stupidity is not thus handicapped. -- Elbert Hubbard
May 30 2023
On Tuesday, 30 May 2023 at 21:42:04 UTC, H. S. Teoh wrote:```d void genSplitHtml(Data data, ...) { auto outputTemplate = File("template.html", "r"); foreach (...) { auto filename = generateFilename(...); auto sink = File(filename, "w").lockingTextWriter; ... while (line; outputTemplate.byLine) { if (line.canFind("MAGIC_TOKEN")) { generateOutput(...); } else { sink.put(line); } } } } ```I didn't know that `while` is used just like `foreach`. 0k4y, we can use `auto` in `while` condition or `if`, which lives in the same block as `auto`. ```d while (line; outputTemplate.byLine) {...} ``` Does the following really work, thanks?```d static struct MockFile { static string[string] files; string fname; this(string _fname, string mode) { // ignore `mode` for this test fname = _fname; } // Mock replacement for std.stdio.File.lockingTextWriter auto lockingTextWriter() { return (const(char)[] data) { // simulate writing to a file files[fname] ~= data.dup; }; } void rewind() {} // dummy void close() {} // dummy auto byLine() { // We're mostly writing to files, and only // reading from a specific one. So just fake its // contents here. if (fname != "template.html") return []; else return [ "<html>", "MAGIC_TOKEN", "</html>" ]; } } ```SDB 79
May 30 2023
On Wed, May 31, 2023 at 12:28:40AM +0000, Salih Dincer via Digitalmars-d wrote: [...]I didn't know that `while` is used just like `foreach`. 0k4y, we can use `auto` in `while` condition or `if`, which lives in the same block as `auto`. ```d while (line; outputTemplate.byLine) {...} ``` Does the following really work, thanks?Oops, that's supposed to be `foreach`, not `while`. :-/ My bad. T -- Trying to define yourself is like trying to bite your own teeth. -- Alan Watts
May 30 2023
On 5/30/23 5:42 PM, H. S. Teoh wrote:So here's one. Today, I was writing some code that iterates over a data structure and writes output to bunch of different files. It looks something like this: void genSplitHtml(Data data, ...) { auto outputTemplate = File("template.html", "r"); foreach (...) { auto filename = generateFilename(...); auto sink = File(filename, "w").lockingTextWriter; ... while (line; outputTemplate.byLine) { if (line.canFind("MAGIC_TOKEN")) { generateOutput(...); } else { sink.put(line); } } } }Since the whole point of this function was to write output to different files (with automatically determined names), I wanted to write a unittest that tests whether it creates the correct files with the correct contents. But I didn't want unittests to touch the actual filesystem either -- didn't want to have to clean up the mess during development where the code might sometimes break and leave detritus behind in a temporary directory, or interact badly with other unittests running in parallel, etc..All of iopipe is 100% unittestable, because all strings are treated as input pipes. Roughly speaking, if this were iopipe, I would do it like: ```d // returns an iopipe with the translated data auto genSplitHtml(Input)(Input inputPipe, Data data, ...) { ... } // and then you can call it like: File("template.html", "r").genSplitHtml(data).writeTo(File(filename, "w")).process(); // in unittest auto result = "contents of template".genSplitHtml(data); result.process(); // oof, would be nice if I could do this in one line assert(result.window == expectedData); ``` I need to work on it more, that `writeTo` function is not there yet (https://github.com/schveiguy/iopipe/issues/39) and the file opening stuff isn't as solid as I'd like.D not only r0x0rs, D boulders!!Indeed it does! -Steve
May 30 2023
On Wednesday, 31 May 2023 at 00:47:27 UTC, Steven Schveighoffer wrote:More than admit it, I want it to look cool and humble at the same time. IMO this wil be possible with [IVY](https://forum.dlang.org/thread/lktxmzdcmaagtrswpily forum.dlang.org)... I am working on a project of about 1 Kloc. The project is so simple (thanks to D!) that it is unnecessary to explain. However, since I am working with string and dchar, I will add JSON and File System simultaneously in the future. For now, I don't need it at all and I'm not in a rush. Even a simple wrapper below makes it easy for me to use in a foreach. I like to write code with D. ![snippet](http://www.sdb.net.tr/dlang/images/stripRange001.jpg) SDB 79On 5/30/23 5:42 PM, H. S. Teoh wrote: D not only r0x0rs, D boulders!!Indeed it does! -Steve
May 31 2023
On 5/30/23 14:42, H. S. Teoh wrote:So here's one.Thank you for reminding us. ;)void genSplitHtml(File = std.stdio.File)(Data data, ...) { ... }Smart! In the past, I used runtime polymorhism to achieve the same. Oops! :)MockFile can be expanded in the future to simulate, e.g. a full filesystem,Yep. Mine was using associative arrays keyed by strings (file names) and data as dynamic arrays. Ali
May 31 2023