Software Development

Legacy Code to Testable Code #3: Adding Setter Accessors

Irish Setter. He's a go-getter.
Irish Setter. He’s a go-getter.

This post is part of the “Legacy Code to Testable Code” series. In the series we’ll talk about making refactoring steps before writing tests for legacy code, and how they make our life easier.

Adding accessors to private state data is an admission that either our design is wrong, or that we’re adding the accessors purely for testing. If that state was private before, how come are we exposing it? And if so, maybe tests are not the only clients of this data.

We need an accessor in two cases:

  • When we need to inject values that are otherwise hard to inject
  • When we need to probe the internal state to understand the impact of our operation

In both cases, we’re adding functions that were not needed before. Meaning, the caller did not need to either inject or probe the data. Now our tests, another client, demand an additional entry point. In this post, I’ll concentrate on the Set accessors for injecting values.

Injecting values

If we have other methods, we might not need an accessor. For example, we have a private field that’s initialized in the constructor:

public Account(Bank bank) {
    this.bank = bank;
    ...

If we pass the new bank value as a parameter, we can set it in our tests easily to whatever we need. However, if the constructor looks like this:

public Account() {
    this.bank = new Bank();
    ...

The seam for inserting our bank is no longer there, and we need another way to inject it. In another case, the new bank can come from an external source:

public Account() {
    this.bank = BankFactory.getBank();
    ...

In both of these cases, we can mock the creation or the static call with power tools. If we can’t use those, we can introduce a “setter” method. This “setter” can be public or at least accessible to our test.

public setBank(Bank bank) {
    this.bank = bank;
}

When we call this method, it overrides the value that the constructor initialized. This doesn’t work if our constructor already did something with the original value. Usually it’s not a problem, because we’re testing code in another method, so whatever the constructor did, the tested method will be under our influence. In other cases, we can mock the constructor using partial-mocking for eliminating code that runs in the constructor, or use power tools to mock the factory method.

From here it gets complicated. What if our `bank` is static? Consider a private static singleton:

private static Bank theBank = new Bank():

Once initialized, it cannot be replaced through regular means. If our test calls for it, replacing it can be easier by injecting our own mockBank. So we can add a static “setter”:

public static void setBank(Bank bank) {
    theBank = bank;
}

As before adding an external “setter” is a risk: What if someone else decides to call it? We can reduce accessibility, but the risk is still there.

In both former cases, we could store the overriding mockBank. It maybe that the value is not stored anywhere, only created on the stack, in the tested method:

public void getAccount() {
    Bank tempBank = new Bank();
    ...

Here, we cannot access tempBank, because it’s on the stack. In this case, we can do a bit of refactoring in order to allow injection. We’ll first extract the creation into a private method called getBank:

private Bank getBank() {
    return new Bank();
}
public void getAccount() {
    Bank tempBank = getBank();
    ...

Using refactoring tools, we can reuse this method for every creation. We can then introduce a field and a “setter”, and modify the getAccount method:

private Bank bank = Null;
public void setBank(Bank bank) {
    this.bank = bank;
}
private Bank getBank() {
    if (this.bank == Null)
        return new Bank();
    else 
        return this.bank;
}

Using this method we skip mocking (and reduce coupling) at the expense of design changes. We can’t escape trade-offs.

There’s a final option for injecting values, if the language supports it: Reflection.  If we use reflection in the test, there is no need for modification in the tested code. However, tests using reflection are as fragile as mocking tests, sometime even more so. Reflection access is fragile, and not refactorable. Therefore, this method should be considered last in the solutions offered.

That’s it for now. Next time we’ll continue the discussion about accessors with “getters”.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button