I think the designer decision for concrete or abstract is proportional to the knowledge of the domain. When you know the domain you know what moves/changes frequently, what is fixed, what has many scenarios, etc and it's easier to find the right balance.
In a scenario where you have a low domain knowledge it is common to make bad decisions on the tradeoff curve, for both sides. In this context I would suggest to got for more concretes design since it is easier to refactoring and avoid premature optimisation (in this case, predicting scenarios that you don't know for sure)
Recently, I came across advice suggesting we should "Strive for unchanging tests" in the book 'Engineering at Google'. This resonated with your discussion about controllability. I believe the aim of creating code that is resilient to change, or can be easily modified when necessary, is not exclusive to tests but extends to any piece of code we produce.
In your view, how closely does this idea of striving for unchanging code align with the controllability you're discussing? Do you see them as two facets of the same principle?
From TestDesiderata.com, tests should be insensitive to structural changes but sensitive to behavior changes. This is more of a design problem than a testing problem.
Controllability for me is about making the state of a computation explicit. If my code has an if statement that depends, implicitly, on data stored in another service, then I can't easily control the code. If at some level of the code that data is passed explicitly as a parameter to a function, then I can control that part of the computation.
I'm not sure I answered your question. Please ask more.
Thanks for your insights, Kent. The distinction you draw between unchanging tests and controllability is illuminating, and I appreciate the nuanced approach of the test Desiderata as compared to simply stating "strive for unchanging tests."
Your perspective on controllability brings to mind the interplay between data and interface. It seems to me that if a data structure already adequately abstracts the underlying dependencies, it could serve as the ideal point of balance between abstraction and concreteness, thereby enhancing controllability. However, in the absence of such a data structure, relying on an interface might be the necessary course of action.
In your experience, what are some guiding principles or considerations when deciding between data and interface for enhancing controllability?
I could still say both examples, the socket connection, and the "customer" are the same.
For the "outside" world, what is important is "where you communicate with", "whom you are talking about".
The fact that your implementation needs a "socket" object from IP/PORT, or a processed version of your customer is the same.
What you change is what is taken as argument to the function you test.
You could still have your
```
main_function(ip, port)
main_function(customer)
```
But instead of deeply test `main_function` directly, you test `subfunction` that works on easier to test parameters.
You can still have one test for `main_function` but once you tested the socket connection creation/the customer important things extractor, no need to test all the combinations on this one.
The question is, can the outside world cope with your better to test interface.
Being able to move that interface you want to run your tests against to the main function, or just leave it to sub-functions.
Some patterns may simplify this, like typical factory pattern where users would only create their object with `from_customer`, `from_ip_port`, and only your tests would call it without the factory (except once to test the factory). But its in practice the same as "main_function_wrapper(ip, port)" -> "subfunction(connection)" where you would only test subfunction.
I often rely on switching from operation on an object/having external side-effects, to testing a pure subfunction only working on stdlib objects where I can really easily spam tests (even abusing python `doctest` to test all in the docstring).
And the "side-effect/object specific" unwrangling just needs one test to show it handles modifying a real object.
I like this kind of specific advice. I wonder where I can read more of your content on software design. Is tidy first? book coming up the best source, or prior blog posts or books you’ve written may have more of this low level content? I enjoy higher level concepts too, but I’m particularly interested in how to write better code.
Implementation Patterns, Smalltalk Best Practice Patterns, any of my talks about Responsive Design. Tidy First? will be the most up-to-date compilation of my thinking.
This kind of specific topic is great because it’s easy to understand and put into practice. I enjoyed reading your book, Test Driven Development: By Example, for the same reason. Implementation Patterns is next on my reading list. Thanks for the recommendations.
Isn't this where the idea of interface segregation can be applied? The customer object has too much information given what is required for the mortality lookup. If the mortality table took a MortalityLookupSpec which provided the necessary bits then the test can just pass that, and changes to the spec are not too painful. Customer could even implement the interface which may work from a system perspective but would seem a little weird.
Then the solution becomes about the most appropriate abstraction rather than abstract v concrete
You could use interface segregation instead of passing 2 parameters. If those 2 parameters only ever occur together in this one function invocation, then making an interface for them right now seems like costs with few benefits. If the pair of parameters start popping up elsewhere, that is a good time to retrofit the interface (or a separate object).
(2) If Customer violates Heuristic of Demeter, then yes, setting up the mock would be complicated, and there would be motivation to go for concrete parameters.
E.g., if retrieving gender was `customer.x().y().getGender()` or other complex logic.
Does the above make sense? ... And if yes, what motivations or examples would there be for going for concrete parameters for controllability outside of objects violating Demeter? (.. Perhaps there're cons to using the Mockito pattern in the first place?)
Having read the automatically translated version I must say I'm impressed. You lay out a path to empathy for those with a different understanding of testing than your own.
Programmers who sneer at testing seem to often also believe that they aren't responsible for the bugs they write. Some organizations encourage this kind of thinking. If you disagree about who is responsible for the code working, you'll certainly disagree about testing.
"if my gut tells"...
I think the designer decision for concrete or abstract is proportional to the knowledge of the domain. When you know the domain you know what moves/changes frequently, what is fixed, what has many scenarios, etc and it's easier to find the right balance.
In a scenario where you have a low domain knowledge it is common to make bad decisions on the tradeoff curve, for both sides. In this context I would suggest to got for more concretes design since it is easier to refactoring and avoid premature optimisation (in this case, predicting scenarios that you don't know for sure)
Recently, I came across advice suggesting we should "Strive for unchanging tests" in the book 'Engineering at Google'. This resonated with your discussion about controllability. I believe the aim of creating code that is resilient to change, or can be easily modified when necessary, is not exclusive to tests but extends to any piece of code we produce.
In your view, how closely does this idea of striving for unchanging code align with the controllability you're discussing? Do you see them as two facets of the same principle?
From TestDesiderata.com, tests should be insensitive to structural changes but sensitive to behavior changes. This is more of a design problem than a testing problem.
Controllability for me is about making the state of a computation explicit. If my code has an if statement that depends, implicitly, on data stored in another service, then I can't easily control the code. If at some level of the code that data is passed explicitly as a parameter to a function, then I can control that part of the computation.
I'm not sure I answered your question. Please ask more.
Thanks for your insights, Kent. The distinction you draw between unchanging tests and controllability is illuminating, and I appreciate the nuanced approach of the test Desiderata as compared to simply stating "strive for unchanging tests."
Your perspective on controllability brings to mind the interplay between data and interface. It seems to me that if a data structure already adequately abstracts the underlying dependencies, it could serve as the ideal point of balance between abstraction and concreteness, thereby enhancing controllability. However, in the absence of such a data structure, relying on an interface might be the necessary course of action.
In your experience, what are some guiding principles or considerations when deciding between data and interface for enhancing controllability?
I could still say both examples, the socket connection, and the "customer" are the same.
For the "outside" world, what is important is "where you communicate with", "whom you are talking about".
The fact that your implementation needs a "socket" object from IP/PORT, or a processed version of your customer is the same.
What you change is what is taken as argument to the function you test.
You could still have your
```
main_function(ip, port)
main_function(customer)
```
But instead of deeply test `main_function` directly, you test `subfunction` that works on easier to test parameters.
You can still have one test for `main_function` but once you tested the socket connection creation/the customer important things extractor, no need to test all the combinations on this one.
The question is, can the outside world cope with your better to test interface.
Being able to move that interface you want to run your tests against to the main function, or just leave it to sub-functions.
Some patterns may simplify this, like typical factory pattern where users would only create their object with `from_customer`, `from_ip_port`, and only your tests would call it without the factory (except once to test the factory). But its in practice the same as "main_function_wrapper(ip, port)" -> "subfunction(connection)" where you would only test subfunction.
I often rely on switching from operation on an object/having external side-effects, to testing a pure subfunction only working on stdlib objects where I can really easily spam tests (even abusing python `doctest` to test all in the docstring).
And the "side-effect/object specific" unwrangling just needs one test to show it handles modifying a real object.
I like this kind of specific advice. I wonder where I can read more of your content on software design. Is tidy first? book coming up the best source, or prior blog posts or books you’ve written may have more of this low level content? I enjoy higher level concepts too, but I’m particularly interested in how to write better code.
Implementation Patterns, Smalltalk Best Practice Patterns, any of my talks about Responsive Design. Tidy First? will be the most up-to-date compilation of my thinking.
This kind of specific topic is great because it’s easy to understand and put into practice. I enjoyed reading your book, Test Driven Development: By Example, for the same reason. Implementation Patterns is next on my reading list. Thanks for the recommendations.
Thanks! I appreciate your time to go through the comments and provide answers :) I’ll take a look at those references
Isn't this where the idea of interface segregation can be applied? The customer object has too much information given what is required for the mortality lookup. If the mortality table took a MortalityLookupSpec which provided the necessary bits then the test can just pass that, and changes to the spec are not too painful. Customer could even implement the interface which may work from a system perspective but would seem a little weird.
Then the solution becomes about the most appropriate abstraction rather than abstract v concrete
You could use interface segregation instead of passing 2 parameters. If those 2 parameters only ever occur together in this one function invocation, then making an interface for them right now seems like costs with few benefits. If the pair of parameters start popping up elsewhere, that is a good time to retrofit the interface (or a separate object).
Would tools like Mockito get rid of the motivation to go for more concrete parameters, except for objects that violate Heuristic of Demeter?
(1) With Mockito, could we solve the first case without going for more concrete parameters?
```
class MortalityTableTest {
@Test retrieveMaleNonSmoker() {
// Arrange
// pseudo-code, don't remember the syntax
Customer customer = new Mock<Customer>();
when(customer.getGender()).thenReturn(Gender.Male);
when(customer.getSmoker()).thenReturn(Smoker.NO);
// Act
MortalityTable result= MortalityTable.lookup(customer);
// Assert
assertEquals("QM115", result.getName());
}
}
```
(2) If Customer violates Heuristic of Demeter, then yes, setting up the mock would be complicated, and there would be motivation to go for concrete parameters.
E.g., if retrieving gender was `customer.x().y().getGender()` or other complex logic.
Does the above make sense? ... And if yes, what motivations or examples would there be for going for concrete parameters for controllability outside of objects violating Demeter? (.. Perhaps there're cons to using the Mockito pattern in the first place?)
Having read the automatically translated version I must say I'm impressed. You lay out a path to empathy for those with a different understanding of testing than your own.
Programmers who sneer at testing seem to often also believe that they aren't responsible for the bugs they write. Some organizations encourage this kind of thinking. If you disagree about who is responsible for the code working, you'll certainly disagree about testing.