2

Well, I'm developing a personal project using TDD and Clean Architecture, and I chose to build an abstract class to gather all use-cases behavior in one single class. My question is: What you think about making test for this abstract class? Would you write tests only for its subclasses or would you do a "mock class" just to check its behavior? If any of this, what would you?

I'm gonna let the code just to be more clear what I'm trying to say.

export abstract class Interactor<Input, Response> {
  private presenter: Presenter<Response>;
  protected abstract execute(execute: Input): Promise<Response>;

  constructor(presenter: any) {
    this.presenter = presenter;
  }

  public async run(input: Input) {
    try {
      const response = await this.execute(input);
      return this.presenter.showSuccess(response);
    } catch (error) {
      if (error instanceof ApplicationError || error instanceof DomainError) {
        return this.presenter.showError(error);
      }
      throw new Error("Unexpected Error");
    }
  }
}

Edit1: the "execute" method is protected.

5
  • Not really duplicates, but related: Testing abstract class' behavior, and Should I unit test my subclasses or my abstract parent class?
    – Doc Brown
    Commented Jul 23, 2022 at 16:50
  • I read these ones too, but one difference between theirs and mine is that I'm trying to gather some components I wrote with TDD. For exemple: a presenter is in the Adapters layer in CA, but in my code, I'm requesting it to build an instance of the use-case class. So, I wouldn't be testing just the use case, right? I would be testing the interactor too, cause' my "execute" function is protected, the only public one is the "run". If someone has an opinion about this method I'm following, it would be really helpful and constructive for me. Commented Jul 23, 2022 at 17:21
  • "a presenter is in the Adapters layer in CA, but in my code, I'm requesting it to build an instance of the use-case class" - your scenario is perfectly fine in CA terms. You see, the implementation of the presenter is in the Adapters layer, but the presenter is an implementation of a push-based output port interface (let's call it IPresenter) that resides in the Use Cases layer and is owned by (or specified by, required by) the Interactor. In your code, the output port is implicit, as it's not represented by a type (you specified the parameter as any). 1/2 Commented Jul 25, 2022 at 18:01
  • But it's nevertheless there - it's conceptually the collection of public methods you call on your presenter parameter { showSuccess, showError }. Note that this is exactly what's depicted in the lower-right corner in this image. It's the DI principle: the interactor needs to call the presenter - but cannot depend on it directly, instead, it relies on the fact that the instance given to it implements a certain set of methods - the interface specified in the UseCases lyr, but implemented in the Adapters lyr. 2/2 Commented Jul 25, 2022 at 18:01
  • P.S. Just to drive the point home: even though the parameter type is any, if you actually pass an object that doesn't have { showSuccess, showError }, your interactor will break - which illustrates that it does depend on an interface, it's just that this interface is not explicitly represented in code. (I'm using the word interface in the traditional sense - the public API of an object, the set of it's public methods + their semantics.) Commented Jul 25, 2022 at 18:06

6 Answers 6

5

Take a step back and look at your context:

  • as an application programmer who has access to all concrete subclasses of that abstract class, it will be usually sufficient to write tests for those concrete subclasses. Of course, you should check that your tests achieve a sufficiently large coverage for the code within the abstract class.

    In your specific example, I would expect most tests for the subclasses call the run method, since it is the only way to call "execute" through a public interface. Hence, the try section will be covered already, no need to add another subclass for this purpose. However, if there is no test for the catch part, and adding another "mock" subclass for exactly that purpose seems to be the most simple testing approach, then I would recommend to do exactly that.

  • as a library vendor who provides the abstract class and expects users of your lib to implement subclasses by themselves, you simply cannot write tests for those subclasses, since you will never get access to them them. Hence your best approach is to write enough tests using "example derivations", not just for the abstract class itself, but also for testing your libraries code which makes use of the abstract Interactor.

3
  • I concur. As a library vendor (or maybe, internal provider), you definitively have documented the acceptable ways to use your abstract class. You should be having "example derivations" for each of those ways in your test code. These can also be very valuable in end user documentation.
    – tofro
    Commented Jul 24, 2022 at 12:04
  • @tofro: I do not recommend relying on unit tests for documentation. When providing a library to the public or others in your organization, real API documentation with examples is what you need. Tests are not a substitute for end user documentation. Commented Jul 24, 2022 at 12:11
  • 1
    Well, if you read again that's not what I'm saying. TDD needs a test to drive implementation of something, otherwise it won't exist. What you document towards the end user is your requirement that drives these tests (when providing libraries) - Or, put the other way round: If you don't document a functionality in your API docs (because you don't intend to provide that), there's no reason to implement a test and thus no reason to implement the function at all.
    – tofro
    Commented Jul 24, 2022 at 12:18
3

Test behavior. Focus on that and many structural issues can be ignored.

Now sure, all testing happens against some interface, some boundary. And likely there is more than one choice of interface/boundary that still gets to the same behavior. That structural choice has some implications.

Would you write tests only for its subclasses or would you do a "mock class" just to check its behavior? If any of this, what would you?

Tests that fully isolate the abstract class, mocking out any dependencies or children have a lot of pros. They leave you with little code under the test to read. That makes them easy to debug and typically fast since they don't do much. But they have a lot of problems. Chiefly they know a lot of implementation details. They have to to mock all those collaborators. Which is really bad since under this style you'll also need a lot of tests. Test like this and making any implementation changes, that aren't concealed / abstracted by the one class, become impossible without deleting or redesigning tests. This testing style can actually drive people to work more procedurally since extracting a new class forces the creation of so many more tests.

Folwer called these Solitary Tests. He contrasted them with what he called Sociable Tests. Tests that stepped back from the class boundary and let many classes work together under the test (as a unit). These kinds of tests allow more to be abstracted away from them and so more implementation is hidden and available to be refactored without breaking the test.

That comes with a cost as well. More code falls under the test. Thus it can take longer to find a bug since there is more code to read. Tests can take longer to run and so they are run less frequently.

Your question boils down to the same choice. You've simply focused on the case where this is happening to an inheritance structure. You can still fully isolate. But should you?

Fowler favored the Social Tests but acknowledged you can use both. My problem with that is when you mix the two kinds of tests in the same bucket. If you're going to do both I say keep these tests segregated. The "fully isolated/solitary/developer" tests are fast but more likely to need deleting as things change. They can do a lot of damage just by making people reluctant to get rid of them. You can help with that by not mixing them with the slower longer lasting Social ones.

As for which you should choose in this particular case, that fully depends on your needs. Do you, right now, need some focused help developing the behavior inside this abstract class without being distracted by the children? Or do you need to ensure you're getting the behavior you need even when children are involved?

Like I said, you can do both. Just please don't mix these testing styles together in the same place. It gets confusing.

1
  • Thanks for the answer... That was really helpful! Commented Jul 25, 2022 at 10:44
2

When testing a class directly, you only need to test the logic inside that class. For the Interaction<T1, T2> class the logic is primarily error handling.

You could create a mock child class to test the error handling behavior, but that does not ensure the concrete classes exhibit that same behavior. A derived class could handle exceptions before allowing them to bubble up to the run method in the abstract parent.

For this reason I would test each derived class individually to ensure the behavior of the abstract parent is actually allowed to execute in those error scenarios.


As an aside, remember to pass the original exception when raising new Error("An unexpected...").

throw new Error("An unexpected...", error):
//                                ^^^^^^^
1
  • My doubt when building this was really about what should I do after doin' the test, the concrete use-case class, or the interactor abstract class, once the first one would extend from the second one. And just being more clear about what I've done to test the use-cases: I'd put the abstract method "execute" as public to test just the unique use-case behavior, not the entire class. 'Cause when I call the "run" method, it runs the "execute" one first, which is implemented in the use-cases class. If any individual use-case throws, it'll show in the presenter, otherwise, it'll show success. Commented Jul 23, 2022 at 16:43
0

What you would do is write unit tests against the interface of the abstract base class. And then you run each test once for an instance of every possible subclass.

Obviously if subclass X has a bug in its method M which is part of the interface of the abstract base class, your unit test will only fail if you call M on an object that is an instance of X, so that’s what your unit tests need to test.

There may be things you can test with an abstract class (and either a simple derived class or a mocked derived class). For example if you have methods “name”, “capitalisedName”, “uppercasedName” you cannot test the first method, but the other two.

0

When ever you find it hard to test some of your code you should step back a little an think about your design.

In your case your code fails to follow the Favor Composition over Inheritance best practice. Instead of forcing the user to extend your class you could expect an Executor interface implementation and call the execute method on that.


BTW: I really doubt that your code really is the result of TDD. I for myself never came up with a public abstract class meant to be extended by others when doing TDD.

-1

TypeScript lets you do this:

class A
{
   public methodToTest() { console.log("hello world"); }
}

abstract class B extends A
{

}

class C extends B{

}

var c = new C();
c.methodToTest();

Now I can instantiate A in my tests and test the logic of the method in the un-instantiable abstract class B.

*keyboard drop *

But lets imagine for some reason your PR gets rejected. Really here you are calling into question the very nature of an abstract class.

Should we even expect the abstract classes methods to work without extra implementation in a derived class? probably not! that's the reason we aren't allowed to instantiate an abstract class in the first place right?

So, get with the program and stop using inheritance in your code. It makes it hard to test and hard to use.

Instead, write a injectable service class that performs the shared logic in a testable way and dependency inject or otherwise use that class as part of your composed classes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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