Every large test suite I've seen in every language at every company I've worked with has intermittent or flaky tests. Maybe each test on average is 99.9% reliable. But across the entire suite, you will have some low-level degree of intermittent test failures. Then you need a team of people whose job it is to triage, investigate, and either fix or ignore those failures. This is a complete waste of human capacity, and it's not a very career-enriching task either. Haskell's ability to define restricted subsets of generalize IO is extremely powerful and sidesteps this entire problem.
The power of the technique in the article is that your tests are _guaranteed_ reliable by the type system, because it is no longer possible to accidentally call into some general IO action. Code under test must go through the World API, which is fully and deterministically mocked in tests.
An example of "innocent intermittence" recently came up at my current employer: there was a test that invoked a web service twice, verifying idempotence. It then asserted the responses were identical. This worked almost all of the time, but when the two service invocations straddled a minute boundary, then some output text that included a rendered timestamp (rounded to the minute), which would cause the test to fail.
It's extremely hard to get an engineering organization to just inherently know not to call, say, datetime.now() in their code, especially when the tests pass most of the time.
Wouldn't it be nice if you could just obstruct such patterns within the first millisecond of their first interaction with the compiler?
Et voila, no more time issues.
> developers do not know that they mustn't call
> datetime.now()
Plenty of languages where it's easy to mock the equivalent to that call. > An example of "innocent intermittence" recently came up
> at my current employer: there was a test that invoked a
> web service twice, verifying idempotence. It then
> asserted the responses were identical.
This is not innocent intermittence, this is a bad test. No-one should get in trouble for it, but I would certainly expect senior developers to get involved to find out:Why is idempotence being tested for on the response side rather than on the execution end? If the call is truly idempotent, then a changing date-stamp means that the test has captured the information envelope as well as the data payload, and is testing that, which is sloppy - you'd probably be upset to see the HTTP headers being tested in the same string, so why other pieces of the envelope too? If a test is calling a "web service", then it's either an integration test (so why's it testing for idempotence, rather than testing the envelope?) or it's a unit test gone awry. Are there other tests that do this too? Could this all be abstracted out in to a sensible library to make it easier for more junior developers to not make these mistakes?
The moment you start accepting less than 100% test pass rates as being OK, the moment madness descends and you lose much of the value of your test suite.
Except it doesn't, precisely where that problem is most annoying. For example, suppose you want to test a scheduler, or any mechanism that places a timeout on an operation. You could abstract away the entire timing mechanism, but that is exactly what you're trying to test: maybe the developer used some system function wrong, and is now waiting 5 seconds instead of 5 milliseconds?
This approach (which is the same as the mocking approach -- there's nothing special about Haskell here) doesn't do too well when you want to test timing, and those tests are usually the most brittle (hosted continuous integration services often introduce really long pauses). Of course, you could weave virtual time services throughout your code, but then you have the third-party library problem again.
Mocking (or IO isolating as you call it in Haskell) ends up producing really good unit-tests, but often the more important tests span more than one small unit, and making those non-flaky is very hard.
In theory, you could replace all references to time in a JVM language -- even in libraries -- using an agent or a bootstrap classloader in order to make larger tests run predictably, but I'm not aware of people going to such great lengths (maybe Google does it; I know, for example, that when it comes to accessing the filesystem, they replace the JVM's filesystem provider with an in-memory file-system[1] when they don't mock, but I don't know how the deal with clocks).
EDIT: Apparently somebody has done just that as a research project[2]. It's actually given me an idea for a nice afternoon-project.
[1]: https://github.com/google/jimfs
[2]: http://www3.imperial.ac.uk/pls/portallive/docs/1/55125696.PD...
Time is an IO effect, so it's true that mocking it away removes that test. I'd treat that kind of "do I understand the API?" issue as an integration test, and split the problem up a little:
- "Mock" the timeout calls, to allow as many tests as possible to run outside IO
- Write tests where the operation always succeeds (eg. "Just x")
- Write tests where the operation always times out (eg. "Nothing")
- Write tests which do a combination of success/timeout and a bunch of different interleavings (QuickCheck would probably be useful here)
- Write some small integration tests, eg. give a timeout of "5" to a process that sleeps for 5 seconds, it should time out; give a timeout of "5" to a process that sleeps for 4 seconds, it should succeed; and so on.
> Mocking (or IO isolating as you call it in Haskell) ends up producing really good unit-tests, but often the more important tests span more than one small unit, and making those non-flaky is very hard.
Haskell's IO isolation causes the vast majority of a program to be defined outside IO. You're right that the trickiest parts are often in IO, but at least those are pretty much separate from any complex calculation/logic.
Yes, but in imperative languages (with meta-programming) you can disentangle mixed IO and logic even without changing the code (or if it's in a third-party library). The advantage is that it allows you (not always, but sometimes) to support even larger tests in this way.
Getting those tiny, one-function unit-tests to run predictably is not hard in any language; you just make them smaller and smaller and mock everything else. The tricky bit is making larger tests run predictably, and a flexible, hackable runtime has the advantage there. That's precisely where meta-programming shines.
I don't understand what you mean by "mixed", "disentangle" and "change".
I would say "mixed IO and logic" is when one block of code describes both how to calculate some value and which side-effects to perform. "Disentangling" these would mean separating the description of the calculation from the description of the side-effects. Doing that "without changing the code" doesn't make sense to me.
E.g. on the JVM you do that with Mockito (similar mocking libraries exist for other languages/runtimes). Say you have a (silly) method that does computation and IO:
void printDouble(int x, PrintStream out) {
out.println("Result: " + (x * 2));
}
You can then do: @Test
public void testPrintDouble() {
PrintStream out = mock(PrintStream.class);
printDouble(3, out);
verify(out).println("Result: 6");
}
No output is actually done. The call to println (which may be deep in some library printDouble calls rather than directly in the tested code). We've essentially replaced any object that does direct IO with something akin to an IO monad.We can query the mock in more sophisticated ways, too. Here's a very contrived example:
void printDoubleWithLog(int x, PrintStream out, PrintStream log) {
log.println("Computing...");
out.println("Result: " + (x * 2));
log.println("Done.");
}
@Test
public void testPrintDoubleWithLog() {
PrintStream out = mock(PrintStream.class);
PrintStream log = mock(PrintStream.class);
printDouble(3, out, log);
InOrder ordered = inOrder(out, log);
ordered.verify(log).println("Computing...");
ordered.verify(out).println("Result: 6");
ordered.verify(log).println("Done.");
}
This contrived example also verifies the relative ordering of IO done on two separate streams. Again, no API change is necessary, and the actual IO calls may be deep in some library.I view that as pretty trivial though; it's just a case of switching out the language's IO primitives, which are effectively free variables. With most "large" languages (Java, Haskell, etc.), the implementations are usually gnarly enough to make such substitution a significant engineering accomplishment, but that's an artefact of the situation, not the approach. As an analogy, the task of fitting corrective optics to the Hubble space telescope was only difficult because it was the Hubble space telescope; fitting adaptive optics is trivial enough that bespectacled children do it every day.
In light of this, I think that disentangling mixed-up definitions is a much more important and interesting problem. It's non-trivial to extract pure computations from imperative code (whether they're written in Java, or Haskell, or whatever), yet there is a great potential benefit for improving code, in terms of understandability, modularity, re-use, maintainence, etc. The fact that pure code is easier to test, or that side-effects can be substituted for the purposes of tests, is really a minor point in comparison. They are workarounds for running code which has too many responsibilities. Much better to avoid the problem in the first place by writing separate code for separate concerns.
Clear separation of concerns is an important aspect of good code, and every language teaches this practice. I don't think Haskell has any advantage there.
I also think -- and this is probably a point of contention -- that the way Haskell separates pure computation from side-effects is a bit arbitrary. Haskell defines computation in a way similar to lambda calculus, which designates as impure any change to something which may then affect a function's result not through its arguments. In short, it equates the mathematical notion of the function with the software notion of the subroutine. Haskell subroutines are mathematical functions, period (well, except for unsafe etc.). I don't think that this is the best way to describe code, or to define separation of concerns (It also makes Haskell jump through hoops when it deals with things that are natural for computation but don't fit well with the notion of a mathematical function). A good separation of concerns in Haskell is not necessarily the best organization for Java/Python.
I get the feeling you're spouting very strong opinions without ever having actually written real production code in Haskell. Because what you're saying is nonsense.
Anything you can do in an imperative language you can do in Haskell - the only difference is that Haskell allows you to define subsets of IO that are enforced by the type system.
Mocking time is crucial for testing timeouts. You don't want your test to actually take five seconds. You want to rapidly test the code in the case that a timeout occurs before completion of the action and vice versa. You want those tests to be 100% deterministic and fast. With the World approach described you can get that.
Also, there is nothing about the technique described that limits you to unit tests. IMVU tests entire web services with this approach and it works well. "World" in that case includes APIs for accessing Redis, MySQL, the customer's session, time, concurrency, and so on. They're all perfectly faked out in tests, and the tests are 100% reliable.
So far, I've said two things. One is that the technique described in the post, which "uses the power of Haskell", is actually trivially done (and has been used by countless projects for years) in almost all imperative languages, and less intrusively (it doesn't require introducing new types and works with third-party code). I don't know much about Ruby and Python, but the JVM also allows you to define subsets of IO (even restricted by actual files) that are enforced by the runtime, or subsets of IO operations that will be enforced by the compiler. This technique has been used in imperative languages for a very long time. That's not an opinion but a fact.
The second thing I've said is an opinion, and is unrelated to a specific programming language. I said that a lot of very small, fully-faked unit-tests only get you so far, and it's fairly easy to get those tests to run 100% predictable. The more troublesome tests are those that aren't faked, and they are crucial to expose many bugs. That's it. I don't think I've said anoything too controversial.
"is actually trivially done (and has been used by countless projects for years) in almost all imperative languages, and less intrusively"
Is simply not true. In a JavaScript test framework, drawing from top-of-mind, recent examples, you cannot prevent the code under test from replacing document.location.
In Java, as you gave in another examples in this thread, you can't statically prevent access to the RNG or perhaps the current time.
In Python, there's nothing stopping any code from calling into a C module that has arbitrary effects.
Haskell is unique among any languages I've ever used in production that allows deterministic testability as a static, enforced-by-the-compiler guarantee.
I partially agree with your second comment, and I do think other forms of testing (such as fuzz testing, integration tests with third parties, acceptance tests) are valuable, but the technique described can be used in much larger tests than simple unit tests. As I mentioned, IMVU uses it for testing entire web service request handlers.
I get the feeling some people think that World is onerous and expensive to set up, but in practice you just write all of your code in World and it's not boilerplatey or complicated at all. The framework around World is set up once and never touched again.
* Disclaimer: I no longer work at IMVU, but I super super super miss Haskell in production.
> Haskell is unique among any languages I've ever used in production that allows deterministic testability as a static, enforced-by-the-compiler guarantee.
Oh, that's true. It's just that if your tests are guaranteed to be deterministic, I don't see why the fact that the property is enforced statically is so important.
Personally, I think Haskell sacrifices a lot for the sake of static guarantees (including the ability to reason about programs not before they run, but during and after). Also, its reliance on Curry-Howard for verification is limited and narrow-sighted, and its insistence on referential transparency everywhere is interesting yet misguided. I'm now preparing a talk for the Curry On/ECOOP conference where I'll show -- among other things -- how all monads can be easily translated to imperative constructs (I won't go into details here, but what you need is continuations and continuation-global variables) in a way that makes them more understandable and compose more naturally than in PFP (i.e. there's no need for monad transformers). Even though this model does not equate the subroutine with the mathematical function -- as PFP does -- it is no less verifiable than the PFP approach. But I digress...
I'm caustin@gmail.com
But my experience with mocks is that expectation-based mocks are fragile and make it easy to write tests that pass but are fundamentally wrong; if you just say you expect XYZ to come out of the database, even if your code gets refactored to not insert XYZ into the database earlier, the test will pass despite the code being completely broken.
With the haskell World approach, you don't have that - the fake database is fake because it's in memory (and thus very fast and uses a blank state for each test), but it's actually implementing the same semantics as the real database.
Some of the other IO stuff we've encapsulated is just expectation-based mocks and as you said that's not new, but it's still clean and doesn't require any changes to the code under test to make it mockable, which we did when we used mocks in our PHP.
[1]: https://github.com/google/jimfs
Granted, Haskell's type system goes a long way to helping with that (often a function of type A -> B only has a handful of possible definitions given sufficiently complex types A and B), but it's still worth using actual production-use code paths when feasible.
EDIT: Where I work, we have a large amount of tests, but we have pretty much never deployed an instance of our application without having every test pass.
The idea of having 100% of your tests pass is a lot easier when it's the default, and when you're doing more continuous integration.
If you're using a language like Haskell, your business logic will already be pretty separate from the ickiness of the IO layer, so most of your tests will be deterministic and nice by default.
If you're using a messier language... well there's a lot of test suites out there. If you're using something that's mainly Django/Rails-y stuff, you don't have many excuses not to be 100% passing by default. Most major frameworks/languages offer extensive mocking tools and the like to avoid non-determinent behavior in tests.