¿ªÔÆÌåÓý

ctrl + shift + ? for shortcuts
© 2025 Groups.io

Re: "bottom-up" TDD and common behaviors


 

¿ªÔÆÌåÓý

TL;DR: For various reasons, I was trying to avoid the use of mocks in this app. I¡¯m asking how a particular problem would be nicely solved without them.?

I should note that I have a guideline that a test should contain no words except those that highlight what¡¯s special about it - about what the test is *for*.

------------------


Let me explain the issue better, or at least more. Here is a form:



That represents one row from an `animals` table. It also represents two rows from a `service_gaps` table, specifically two rows with a foreign key that points at the animal.?

Ecto is an Elixir structure-to-relational-table mapper. It¡¯s easy to ask it for the animal plus its service gaps, in which case it does all the appropriate joins and produces a structure that looks like this (with some fields stripped for concision):


That looks different than what the form displays. Somehow, the nil `out_of_service_date` has been turned into the string ¡°never¡±, and the `span` structure in the service gap has been ¡°flattened¡± into two strings. That is, you could imagine that what¡¯s actually rendered is a derivative structure like this:


If you imagined that, you¡¯d be wrong. Ecto really wants you to put the actual database fields and also the ¡°virtual¡± fields that hold form values into a single structure that looks like this:


***Whether it's a good idea for a database mapper to know a bunch of things about how the client works is NOT at issue here. Ecto is what it is.***

When you read an animal from the database, it comes back as the previous structure with the three highlighted lines set to `nil`. That¡¯s suitable for lots of operations on animals, but it¡¯s not suitable for producing a form that can be readily used to update an animal and its service gaps. For that, you need to derive the appropriate values for the highlighted fields (fill them in as shown). Once you do that, it¡¯s *really easy* to write code that takes the result of an HTTP POST and updates the database: atomically applying all the changes from the form data, including creating a new ¡°service gaps¡± row. (The rather grotty data structure makes for simpler code, which is why I still don¡¯t know what to think about the design of the Ecto library.)

So let¡¯s consider a function that converts an animal `id` into an initialized form. That function is called when a user clicks on a table element like this:


¡­ and it produces an `Animal` structure like the one above. Here¡¯s an implementation of that function:


It has two responsibilities:?

1. Fetch the corresponding animal in the normal way, including the associated service gaps (etc.)?
2. Augment that data to make it suitable for a form.

In a mockish approach, I¡¯d TDD that with pseudocode like this:

? ?updatable(5, ¡°test¡±) produces `..result..`
? ?provided
? ? ? ?Read.one([id: 5], ¡°test¡±) produces `..animal..`, and
? ? ? ?Read.put_updatable_fields(`..animal..`) produces `..result..`

(The `..animal..` notation is for what I call ¡°metaconstants¡±, which are values about which nothing is known except their name and that two instances of the same name refer to the same value. My Midje library for Clojure has metaconstants built in, so you *can¡¯t* do anything else with them. In a dynamically-typed language, it¡¯s easy to use strings or symbols for the same thing. Static typing makes metaconstants much harder.)

I like this test because it doesn't set up specific animal data that gets checked for facts that *imply* that updatability was handled correctly. It says it directly: ?¡°An updatable animal N is an existing animal that¡¯s read and then modified to be updatable¡±. I like to think of such tests as being like proofs that use lemmas: *if* animal reading works correctly, and making an animal updatable works correctly, then fetching an updatable animal is works correctly.?

(That is, I¡¯m working at the conceptual level, not the implementation level, so the ¡°you¡¯re just testing implementation¡± argument against mocks doesn¡¯t apply. After all, *shouldn¡¯t* our implementations be structured around relevant concepts?)

However, for various reasons, I was trying to avoid the use of mocks in this app. So, if you were doing this in a non-mockish way, what tests would you have? (Remember, there are four functions that do pretty much the same thing.)



Join [email protected] to automatically receive all group messages.