Writing/implementing an API: testability vs information hiding

痞子三分冷 提交于 2019-12-02 03:56:29

问题


Many times I am involved in the design/implementation of APIs I am facing this dilemma.

I am a very strong supporter of information hiding and try to use various techniques for that, including but not limited to inner classes, private methods, package-private qualifiers, etc.

The problem with these techniques is that they tend to prevent good testability. And while some of these techniques can be resolved (e.g. package-privateness by putting a class into the same package), others are not so easy to tackle and either requires reflection magic or other tricks.

Let's look at concrete example:

public class Foo {
   SomeType attr1;
   SomeType attr2;
   SomeType attr3;

   public void someMethod() {
      // calculate x, y and z
      SomethingThatExpectsMyInterface something = ...;
      something.submit(new InnerFoo(x, y, z));
   }

   private class InnerFoo implements MyInterface {
      private final SomeType arg1;
      private final SomeType arg2;
      private final SomeType arg3;

      InnerFoo(SomeType arg1, SomeType arg2, SomeType arg3) {
         this.arg1 = arg1;
         this.arg2 = arg2;
         this.arg3 = arg3;
      }

      @Override
      private void methodOfMyInterface() {
         //has access to attr1, attr2, attr3, arg1, arg2, arg3
      }
   }
}

There are strong reasons not to expose InnerFoo - no other class, library should have access to it as it does not define any public contract and the author deliberately didn't want it to be accessible. However to make it 100% TDD-kosher and accessible without any reflection tricks, InnerFoo should be refactored like this:

private class OuterFoo implements MyInterface {
   private final SomeType arg1;
   private final SomeType arg2;
   private final SomeType arg3;
   private final SomeType attr1;
   private final SomeType attr2;
   private final SomeType attr3;

   OuterFoo(SomeType arg1, SomeType arg2, SomeType arg3, SomeType attr1, SomeType attr2, SomeType attr3) {
      this.arg1 = arg1;
      this.arg2 = arg2;
      this.arg3 = arg3;
      this.attr1 = attr1;
      this.attr2 = attr2;
      this.attr3 = attr3;
   }

   @Override
   private void methodOfMyInterface() {
      //can be unit tested without reflection magic
   }
}

This examply only involves 3 attrs, but it is pretty reasonable to have 5-6 and the OuterFoo constructor would then have to accept 8-10 parameters! Add getters on top, and you already have 100 lines of completely useless code (getters would be also required to get these attrs for testing). Yes, I could make the situation a bit better by providing a builder pattern but I think this is not only over-engineering but also defeats the purpose of TDD itself!

Another solution for this problem would be to expose a protected method for class Foo, extend it in FooTest and get the required data. Again, I think this is also a bad approach because protected method does define a contract and by exposing it I have now implicitly signed it.

Don't get me wrong. I like to write testable code. I love concise, clean APIs, short code blocks, readability, etc. But what I don't like is making any sacrifices when it comes to information hiding just because it is easier to unit test.

Can anybody provide any thoughts on this (in general, and in particular)? Are there any other, better solutions for given example?


回答1:


I think you should reconsider using reflection.

It has its own downsides but if it allows you to maintain the security model you want without dummy code, that may be a good thing. Reflection is often not required, but sometimes there is no good substitute.

Another approach to information hiding is to treat the class/object as a black box and not access any non-public methods (Though this can allow tests to pass for the "wrong" reasons i.e. the answer is right but for the wrong reasons.)




回答2:


My go-to answer for this type of thing is a "test proxy". In your test package, derive a subclass from your system under test, which contains "pass-through" accessors to protected data.

Advantages:

  • You can directly test, or mock, methods you don't want made public.
  • Since the test proxy lives in the test package, you can ensure it is never used in production code.
  • A test proxy requires far fewer changes to code in order to make it testable than if you were testing the class directly.

Disadvantages:

  • The class must be inheritable (no final)
  • Any hidden members you need to access cannot be private; protected is the best you can do.
  • This isn't strictly TDD; TDD would lend itself to patterns that didn't require a test proxy in the first place.
  • This isn't strictly even unit testing, because at some level you are dependent on the "integration" between the proxy and the actual SUT.

In short, this should normally be a rarity. I tend to use it only for UI elements, where best practice (and default behavior of many IDEs) is to declare nested UI controls as inaccessible from outside the class. Definitely a good idea, so you can control how callers get data from the UI, but that also makes it difficult to give the control some known values to test that logic.




回答3:


I don't see how information hiding, in the abstract, is reducing your testability.

If you were injecting the SomethingThatExpectsMyInterface used in this method rather than constructing it directly:

public void someMethod() {
   // calculate x, y and z
   SomethingThatExpectsMyInterface something = ...;
   something.submit(new InnerFoo(x, y, z));
}

Then in a unit test you could inject this class with a mock version of SomethingThatExpectsMyInterface and easily assert what happens when you call someMethod() with different inputs - that the mockSomething receives arguments of certain values.

I think you may have over-simplified this example anyway as InnerFoo cannot be a private class if SomethingThatExpectsMyInterface receives arguments of its type.

"Information Hiding" doesn't necessarily mean that the objects you pass between your classes need to be a secret - just that you aren't requiring external code using this class to be aware of the details of InnerFoo or the other details of how this class communicates with others.




回答4:


SomethingThatExpectsMyInterface can be tested outside Foo, right? You can call its submit() method with your own test class that implements MyInterface. So that unit is taken care of. Now you are testing Foo.someMethod() with that well-tested unit and your untested inner class. That's not ideal - but it's not too bad. As you test-drive someMethod(), you are implicitly test-driving the inner class. I know that's not pure TDD, by some strict standards, but I would consider it sufficient. You're not writing a line of the inner class except to satisfy a failing test; that there's a single level of indirection between your test and the tested code doesn't constitute a big problem.




回答5:


In your example it looks like the Foo class really needs a collaborator InnerFoo.

In my opinion the tension between information hiding and testability is solved by the "composite simpler than the sum of its parts" motto.

Foo is a facade over a composite (just InnerFoo in your case, but does not matter.) The facade object should be tested on its intended behaviour. If you feel that the InnerFoo object code is not driven enough by the tests on the behaviour of Foo, you should consider what InnerFoo represents in your design. It may be that you miss a design concept. When you find it, name it, and define its responsibilities, you may test its behaviour separately.




回答6:


what I don't like is making any sacrifices when it comes to information hiding

First, work with Python for a few years.

private is not particularly helpful. It makes extension of the class hard and it makes testing hard.

Consider rethinking your position on "hiding".



来源:https://stackoverflow.com/questions/5031912/writing-implementing-an-api-testability-vs-information-hiding

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!