Most Liked Responses
krasenyp
Man, don’t get me started on mocking. I’ve been looking at poor designs for years and the only way to test anything at all is by using mocks. You talk about DI in relation to mocking, which is great, but I also miss programming against a contract which can be achieved through protocols.
tfwright
Agree with describe with function name but I also include the specific input data case (with foo and bar) because I prefer to test those in isolation to avoid cascading failures and ballooning setup.
I have tried strictly using contexts for setup and just find it too cumbersome, although I grant it seems more pure and I also avoid libraries for struct generation that “compete” with contexts. I write whatever code is most convenient for the case. Sometimes that’s a context function, mostly it’s a private helper calling insert!. Have yet to be bitten by this.
I am on team “don’t write application code for the sake of tests” so I just mock stuff. It’s a tradeoff, but I’d rather my code running in prod as as univocal and expressive as possible, even if that costs some cycles. But that’s from writing code for 25 years before AI came along, I find some of my personal “guidelines” bending quite a bit recently.
fuelen
I would ask the opposite question. Do you want a test suite that checks your code in states the system can actually reach in production, paying for it with some slower setup, or do you want fast tests that run on data no business flow could actually produce? “Just write a valid setup by hand” is easy to say. So is “just write bug free code”. If either was realistic, we would not need tests in the first place ![]()
Context functions carry a contract, and for anything non trivial that contract is much simpler than putting rows into tables by hand. Let’s say you write a chat and you want a thread with a first message. The function is something like start_thread(sender, recipient, initial_message). Internally it creates the thread, links both users, inserts the message, marks it read for sender and unread for recipient. One line in your test setup vs. a lot of ceremony to recreate that exact state directly in the DB.
So is it really “more complex”? I’d say writing it is less complex. There is more code running, yes. But that code is exactly what reduces the space of valid fixture states down to what the system can actually produce. To me that is the whole point, not a cost.
Heavy, yes, in absolute terms. But ex_machina is not free either, and I think people underestimate this. :org has one :user and has many :project, and :project has :created_by and :approved_by assocs, both pointing at :user. Declare those naively as associations and a plain insert(:project) will create three users where you wanted one. To prevent that you start writing factory logic that walks through the other associations to reuse an existing user, and the simple user: build(:user) one liner turns into something messy inside the factory file. The complexity does not disappear.
My personal take: have a centralized test setup layer built on functions, and by default let it call context functions. When a specific context function is too heavy and is used in most tests (for example, users are created as :pending and must be activated through a separate flow, which is correct for the business but annoying as a prerequisite for every test), I add a lighter setup helper for that case, with a short comment explaining why it skips the normal flow.








