Finally, I’m back in the game (literally?), and working on the Elixir side of things for a change.
So, my Elixir backend is heavily tested using unit and integration test. For now, a total of 83 unit tests assert their way through my code, all way from checking the password hashing result, to making sure I don’t have any typos.
Life wasn’t this good just yesterday. My tests were taking 7.2 seconds to run, which for Ruby people is a dream, but for Elixir folks, we can do better.
So, I want to quickly explore the idea of tracking down the slow down and how I managed to resolve it.
So, in my case, it really wasn’t too hard to track down the slowdown. After integrating the authentication features, which uses
Comeonin.Bcrypt, I noticed that a simple test is taking +300 ms to finish, sometimes +800 ms. Wat?! Why so slow?
Turns out the Crypt lord is just too heavy .. Any Warcraft fans out there?
You see, Bcrypt is slow, and that is by design. If your password database gets compromised for any reason, the hacker will have to use the purposely designed, slow algorithm to apply a rainbow table attack on the data, which would take forever. For example, just to crack a single password, it would probably take a single CPU core an excess of 5 years total.
Well, that sounds great! I mean, even though it’s slow, it is secure… However, while unit testing, we really don’t need this functionality at all. In fact, unit testing is by definition testing your code in small, isolated units. We can leave the actual Bcrypt testing for integration tests.
So, how should we go about this?
One could easily utilize the great mock library to mock the Bcrypt module, and we are done! Unfortunately, mocking in this specific case has tons of drawbacks:
- You can no longer run your tests asynchronously
- You must repeat the mock setup anywhere your test invokes
This is Elixir, obviously we can do better.
I am not really sure if this counts as Dependency Injection, but it sure smells like it. What we basically do is head over to our configuration files and setup the modules which provide us with certain services. Based on the run configuration, we provide a different module!
Finally, head over to your main app module, and do:
That’s about it. Now, instead of referencing
Comeonin.Bcrypt in your code everywhere, you use
MyApp.bcrypt instead. If your tests are running, the function will return the stub Bcrypt module which you have already setup to be speedy. While running in dev or prod configuration, the real Bcrypt module will take the wheel.
For the sake of completeness, this is how I setup the
As you can see, I simply replicate the functions in
Comeonin.Bcrypt that I am using, and replace them with a predictable “hashing” algorithm which runs in the microsecond time order.
Elixir continues to kick ass, whether it was parallelism, functionality, or just pure productivity tools. With this simple trick above, the tests now run in ~1.2 seconds. incidentally, I’ve disabled async testing because I access the DB everywhere, but once I upgrade to Phoenix 1.2.0 and turn on async testing as well, tests might as well run within 800 ms or less.