3

Suppose you have two classes: Alpha, and Bravo.

Alpha constructs a new Bravo class during its own constructor and exposes no public visibility to it.
Bravo has a String named charlie (and a method to Get that String).

Let's say you have set up a test that constructs a new Alpha class. At the end of this test, you want to assert that bravo.getCharlie() is equal to some expected String. How should the test get access to this String?

Should we add a getBravo() method to Alpha just because this test seems to need it?

assertEquals("expectedString", alpha.getBravo().getCharlie());

Should we continue hiding Bravo but add a getter for Charlie inside of Alpha?

assertEquals("expectedString", alpha.getCharlie());

(where alpha.getCharlie() is just a way of writing alpha.getBravo().getCharlie() without making that Bravo getter public)

Any other options?

  • 3
    Where did your Alpha get its Bravo? (Any answer other than "it was passed in the constructor" should be a clue as to why dependency injection is a thing.) – cHao May 7 '18 at 19:59
  • Edited question to show that Alpha constructs a new Bravo during its own constructor. – Wallace Brown May 7 '18 at 20:08
9

You wrote

Alpha constructs a new Bravo class during its own constructor and exposes no public visibility to it

So whatever Alpha does, using Bravo for it is an internal implementation detail, right? Then this

at the end of this test, you want to assert that bravo.getCharlie()

means you ask how to write a test which relies on this internal implementation detail.

In most cases, it is best to avoid this. If possible, write your tests using the public interface of Alpha exclusively, then implementation details can be changed later more easily without the need of changing the test. If bravo.getCharlie() is somewhere used inside Alpha, try to invoke a public method which relies on that result.

(Note the whole thing of making / not making private stuff public just for testing purposes has been asked and answered on this site several times. Start with this one or this one).

1

The option I usually go with, is injecting the dependency. That allows me to inject a mock object during tests, and verify the output that way. I also program to interface, instead of using a concrete class to make mocking easier. You didn't specify language - I'm assuming Java, but all I know is C# - so hopefully it's close enough:

So in your case you would have:

public interface IBravo
{
   string Charlie { get; set; }
}

public class Bravo : IBravo
{
   public string Charlie { get; set; }
}       

private readonly IBravo _bravo;
public class Alpha(IBravo bravo)
{
   bravo = _bravo;
}   

void main()
{
   Alpha alpha = new Alpha(new Bravo());
}

Then in your unit tests you would do the following (using Moq mocking framework):

[Test]
public void Test_Charlie_String()
{
   var bravoMock = new Mock<IBravo>();
   bravoMock.SetupProperty(f => f.Charlie);

   var alpha = new Alpha(mockBravo.Object);

   Assert.AreEqual("expectedString", mockBravo.Charlie);
}
1

Let's say you have set up a test that constructs a new Alpha class. At the end of this test, you want to assert that bravo.getCharlie() is equal to some expected String. How should the test get access to this String?

One of the interesting ideas behind "test driven design": if your test is hard to write, that's a hint that there may be a flaw in your design.

Alpha constructs a new Bravo class during its own constructor and exposes no public visibility to it.

This is a code smell, a hint that something has gone wrong in your code.

The pattern that your code is matching is that you have a side effect (a write to the state Charlie) that you are trying to evaluate in your test. It's actually pretty common to have side effects that can't be read later (for example, a write to the console).

The common pattern for testing such a thing is to use a TestDouble, so that you can capture the behavior.

But for that to work, you need to implement your code in such a way that it is open to extension.

Misko Hevery wrote a number of essays about issues related to constructors; his Top 10 Things Which Make Your Code Hard to tTest would be a good starting point.

Just ask for all of the collaborators you need in your constructor.

The idea is that while this example is closed to extension

Alpha () {
    this.beta = new Beta();
}

making the class open to extension is simply a matter of re-arranging the code slightly.

Alpha (Beta beta) {
    this.beta = beta;
}

However, if Alpha() is part of the published interface, then changing the signature in this way may break backward compatibility. That is usually a bad idea, so you might instead make this change

Alpha () {
    this(new Beta());
}

Alpha (Beta beta) {
    this.beta = beta;
}

The constructor that is hard to test delegates its work to the constructor that is easy to test.

Of course, this in turn means that the no argument constructor isn't being covered by this test - we've written better code, but our test coverage numbers have gotten "worse". So you have to decide in your mind how to reconcile that.

In my case, I've allowed my self to be comfortable with the recommendations of Kent Beck

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence....

and C.A.R. Hoare

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies....

The default constructor here meets my bar for "obviously no deficiencies"; the code is too simple to be the source of a fault.

-1

To see Charlie you need to be testing Bravo, not Alpha. Charlie is a detail implementation that Alpha is not interested, so your test will also not have any interest on that.

So, if you have a code like:

class Alpha {
    Bravo bravo;
}

class Bravo {
    Charlie charlie;
}

class Charlie {

}

Your tests will be like:

//AlphaTest
assertEquals(expectedBravo, alpha.getBravo());

//BravoTest
assertEquals(expectedCharlie, bravo.getCharlie());
  • The point of the question is that there's currently no getBravo in Alpha -- and whether it's a good idea to add one solely for testing purposes. – cHao May 7 '18 at 21:16
  • Can you explain better the reason for the negative? I think he could add the method, as he asked. If he is interested to test it, I understand that he would like to change his class "contract" and turn the code visible for another classes (even only for testing). As far I understand, this kind of approach is valid when you are not able to refactor the classes to expose only the necessary methods and test them. – Dherik May 7 '18 at 21:27
  • The reason for not adding a getBravo is that it's an implementation detail, and the user should have as few ways as possible of depending on implementation details (particularly if they may soon change). As far as real users are concerned, there's no such thing as "visible only for testing" -- you can all but guarantee some weenie (or...ahem...tester) will write code that relies on being able to getBravo(), and now you have to either accommodate that usage forever, break tests, and/or be perceived as not giving a damn about compatibility. – cHao May 7 '18 at 22:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.