I have an interface Serializer
with methods serialize
and isSerializerFor
. I created a first implementation of this using TDD, and ended up with a nice clean test case fully covering a nice clean implementation. To get a more concrete idea, here is one relevant commit.
Now someone else (who is not used to doing TDD) started with writing the second implementation of this Serializer
interface. This person figured the test needed for the second implementation would have some overlap with my initial test. So an abstract base class for serializer tests was created, holding the methods that are suspected to be common to all serializer test cases.
I'm not happy with this for two main reasons. First of all, the only reason inheritance is used here is for code reuse, which is a big code smell. Secondly, this breaks the TDD cycle. If I now want to create another implementation of Serializer
, and create a derivative of the base test class, I end up having to implement all production code in one step.
On the other hand, simply duplicating the common code in the test classes seems rather odd as well. I'm hoping composition can be used here in a sane way to avoid these problems.
This seems like a reasonably common situation. How would you solve this? What would you do differently?
1 Answer 1
First of all, the only reason inheritance is used here is for code reuse, which is a big code smell.
Different serializer implementations (like SerializerA
, SerializerB
, SerializerC
) lead to different SerializerTest
classes (SerializerTestA
, SerializerTestB
, SerializerTestC
), which are all "Serializer testers", giving your a "is-a" relationship to the common SerializerTester
base class, so inheritance is probably the right tool here.
If I now want to create another implementation of Serializer, and create a derivative of the base test class, I end up having to implement all production code in one step.
I don't think so. You start TDD by creating a test class SerializerTestA
(derived from SerializerTester
, with nothing more than creating a SerializerA
object. This won't compile - test is RED. Then you implement SerializerA
with nothing more than a constructor and empty method implementations for the Serializer
interface - test is GREEN. Next, you add your first test method to SerializerTestA
, which may just delegate a test call to a corresponding method in SerializerTester
. Because of the missing implementations in SerializerA
, your test fails - RED again. Then, you implement the first empty method in SerializerA
, until your first does not fail any more - test status is GREEN again.
So, as long as you don't call any test methods from SerializerTester
needing the full implementation of the Serializer
class you can still do this step-by-step, staying on the classic TDD path.
-
In the environment I am using, which is PHP+PHPUnit, calling tests in SerializerTester is not needed for them to run. PHPUnit will run all test cases in your test class, which includes all inherited ones. I could change the methods in the base class so they are regular methods and not test methods, and then explicitly invoke them one by one. Though then one ends up with those invocations in every test, which is not far from the original situation in terms of duplication.Jeroen De Dauw– Jeroen De Dauw2013年12月20日 15:36:46 +00:00Commented Dec 20, 2013 at 15:36
-
@JeroenDeDauw: The original situation was that you had to duplicate the test implementations, now you only have to duplicate the test invocations, I would consider that as a big difference. Nevertheless, for TDD you need a way to switch your tests "on" incrementally and individually for every serializer class. If that really bothers you still and you want to remove that duplicate invocation code after you have all your tests in place ...Doc Brown– Doc Brown2013年12月20日 18:10:58 +00:00Commented Dec 20, 2013 at 18:10
-
... then you could add an additional
SerializerTesterAll
class, derived fromSerializerTester
, with one test method for each regular method inSerializerTester
, calling that method, and once you have yourSerializerTestA
complete, refactor it by changing its base class fromSerializerTest
toSerializerTesterAll
and remove all the duplicate test invocations.Doc Brown– Doc Brown2013年12月20日 18:14:11 +00:00Commented Dec 20, 2013 at 18:14
Explore related questions
See similar questions with these tags.
isSerializerFor
) and use something like parameterized tests. Anything implementation specific could then be in a different test specific to just that implementation.