Some time ago I manifested my dislike for using ETS tables because I saw them as Singletons, which bring a myriad of problems when testing. However @peerreynders linked me to the Just Create One pattern, spearheaded by our dear Uncle Bob.
I get the main difference. You can ask an app to create a Singleton a million times, and it will always return the same instance. With JCO, you can’t ask the app to create something a million times - you create it once and that’s it.
But in practice, specially when it comes to testing (which is what I am interested in), they are really both the same. You need a mock (mock as a noun) for each one, and once you bring either of them into your app, you basically forfeit concurrent tests because now you have a global state to worry about - which I dislike very much.
How do you test (or perhaps avoid?) something like a Singleton or a JCO instance and still allow for concurrent tests?
My only idea is to do functional injection, but then I have to pass a (huge) list of dependencies to every function. It is rather discouraging from a design perspective, as you literally need to pass around the whole jungle so you can tell a Gorilla to eat his freaking banana.
ETS and Registry are just 2 examples of Singletons (or JCOs) that are widely used in Elixir apps (as well as in my projects as well).
I am aware they may have other uses, but 90% we use them to store state as a singleton. This is the part I want to isolate and test … And if I am really out of luck, perhaps I should consider alternatives? ( the only alternative coming to mind is to save state in a GenServer process and test the inter-process communcation’s protocol but then I would be spawning processes for the sake of testing without any real runtime benefits and I don’t like that either).
I’m not sure how a GenServer would make it less of a singleton though? As I said it’s not really constraint to ETS. The best way around is imho not having a singleton in the first place, like with explicit dependency injection. Or look into some fancy workaround like Ecto.Sandbox does for Tasks in the newest version, which is basically an implicit version of ownership injection.
I don’t agree with this part. The idea behind Just Create One is to replace a global compile time dependency with a local one that is injected at runtime.
If you have 20 tests you can create:
20 distinct ETS tables with distinct handles
20 distinct ETS tables with distinct names
20 distinct GenServers with distinct pids
20 distinct GenServers with distinct names
i.e. one for each test so that you can run the tests concurrently.
That of course means that the process under test has to have the reference (handle/pid/name) injected during initialization and the reference has to be held in the process state. By extension any code using the reference will have it passed from the process state.
but then I have to pass a (huge) list of dependencies to every function.
how big are your functions that they need a (huge) list of dependencies??? This may be symptomatic of an entirely different organizational problem.
I would really like some tutorials or information about this. Testing code that uses ETS tables (or anything that uses them like Registry) is the bane of my existence in the company, as it makes unit tests quite hard to deal with and I hadn’t come up with a decoupling strategy that works good enough yet.
If you have 3 low level functions with side effects (HTTP request, DB operation, etc) and you have a function that is a composition of those 3, then in order to run the latter function you need to inject the dependencies of its pipeline.
Now imagine you combine this function with yet another function … The number of dependencies grows without bound.
You can say something among the lines of “perhaps you are testing the implementation instead of the contract”, but if I am only testing the functions those module’s APIs make public then I know this is not what is happening.
Another issue here is that for a system big enough, testing their public APIs is a nightmare because the input and the outputs generated are so big, that no one really understands what is going on and they end up just copy pasting whatever the system spit and use it as a test…
But I digress… my focus here is finding a good strategy that allows me to test code that uses ETS tables and Registries in an isolated concurrent and fast way.