2

I have a package that provides an object with quite a lot of features owned by it.

Let us say the object is an HTTPServer, and when the user initializes it by providing config and a request handler object, the server would be serving HTTP with added request observabilty, panic handling, and other in-house logics.

Now, if I want to test whether a handler's panic is properly handled, I would need to start the HTTP server, send a dummy request, and set up the observability; all which is unrelated to the panic handling logic. So, besides unit testing a feature of the object being cumbersome, there would also be a lot of reason why a test might fail (which if I understand correctly, is not a good property of a unit test),

Seeing other threads, a common idea would be to separate a particular subfeature to another (private) object, then unit test that object instead.

However, the sub-object would still be owned by the HTTPServer object, and its implementation initialized by HTTPServer during its initialization. To HTTPServer, the sub-object would just be an implementation detail hidden away from the user, and the unit-test of the sub-object does not guarantee that the HTTPServer is initialized and calls the sub-object properly. So, it cannot be mocked and we still need to retest the same functionality?

So, how do you decompose a big object for testing, and how do you use the decomposed objects in the big object's unit test if the whole functionality is still owned by the big object?

3 Answers 3

2

Test behavior.

Panic handling can be dealt with in a separate object that exhibits this behavior.

Your HTTPServer need only take this object (doesn’t have to build it) and delegate its panic behavior to it.

I am writing a library to be used by other applications. Other people's codes would be the one calling the constructor

Bimo Adityarahman

So you need it even when not passed in. I call those known good default values. Doesn't mean it can't be a separate object. Constructing Panic inside HTTPServer just means you're stuck with HTTPServer knowing its concrete implementation exists. And that just means they have to be deployed together.

You can offer more than one constructor if you feel the need to pass in a different Panic object for testing (as a mock or some such). That makes this an overridable default value. But mocking that would be for testing HTTPServer not Panic. It works, but likely not needed.

No, if all you want to test here is Panic just have your test suite build itself a Panic to test.

You can resort to a static factory/service locator for this. But that's only needed if you really want to hide the concrete Panic from HTTPServer. Instead you will fail to hide the concrete static factory/service locator from HTTPServer.

7
  • The exposed interface for the user is still only the HTTPServer constructor and run method. So even if I separate panic handling to another object, the user would still be unaware of this object and unable to pass it to the HTTPServer object. So, the only one who knows and can build the panic handling object is still HTTPServer itself. How could I mock it then? Oct 4, 2023 at 3:31
  • 2
    @BimoAdityarahman: This answer implies the HTTPServer object gets a panic handler object passed through its constructor, so the logic for constructing it is not privately hidden inside the HTTPServer.
    – Doc Brown
    Oct 4, 2023 at 5:44
  • @DocBrown I do not think I am allowed to change the contract of the HTTPServer. The user only cares that my HTTPServer properly handle panic, and should not be bothered to initialize and provide the panic handler to create the HTTPServer object. One possible idea maybe is to have a global panic handler object factory, to be used when HTTPServer initialize the panic handler object, which could be replaced with a mock during testing. I am not sure though whether this is a correct application of the factory pattern. Oct 4, 2023 at 6:18
  • 1
    Needed things should exist before they are needed. You can build panic before HTTPServer and HTTPServer before User. All you need is a place to do it. Changing how things are constructed doesn’t change their contract. That’s about how they are used. Oct 4, 2023 at 11:18
  • @candied_orange To clarify, I am writing a library to be used by other applications. Other people's codes would be the one calling the constructor. I could not assume they are using any DI framework. How HTTPServer is constructed is part of its contract in the sense that changing how the constructor is written would break users' codes. So in this case, I think panic handling object is entirely owned by the HTTPServer. Its implementation is part of the HTTPServer's, it is instantiated whenever an HTTPServer is created, and destructed when the HTTPServer are also. Oct 4, 2023 at 14:38
2

This is a classic tradeoff:

  • on one hand, you want a very lean public interface for a library, a facade, which hides a whole group of interacting objects or classes behind an small number of methods with simple signature which are exposed to a user

  • on the other hand, this interface is actually too lean for creating effective unit tests.

The standard solution I usually recommend is not to take the "don't test implementation details" mantra too literal. Stay pragmatic. Since you want your tests do something you don't want to allow for "normal users", make a distinction between

  • the public interface of your library (exposed to the user)

  • the internal interface (exposed to testing code)

  • "real" implementation details (the things you keep in private and do not test directly)

The distinction between "public", "private" and "internal" is something you find - for example - in C# with exactly these keywords, but other languages/eco systems have similar mechanisms.

For example, it makes sense to split up your large HTTP server object into smaller, testable units internally without changing its public interface. The panic handler could be an internal class of its own, and in case its construction is complex, there could also be a panic handler factory. By exposing these "helper" classes internally to the test code, but not in public, you can satisfy both requirements: the lean public interface, as well as unit testability.

As an alternative, you might consider if the public interface should be a little bit broader. When a component requires a complex setup before it can be tested it, maybe with a slow initialization phase or maybe with allocating network or other resources, one could add an "official" (=public) testing mode to it. The testing mode may only require a quick and simple setup, and use almost no additional resources. One might also add methods for observing certain behaviour during a test - methods which are technically public, but only sensible in a testing context. This also a common way to make a component more testable without exposing too many internals.

3
  • I do agree that testing private internal objects is a tradeoff against "don't test implementation details". I was struggling with testing constructor call, but that was solved with passing factory mock instead. I am not quite sure about this testing mode, is it to help me as the developer or is it to help the user testing? What would this testing mode replace in the HTTPServer context? Oct 6, 2023 at 7:38
  • @BimoAdityarahman: such a "testing mode" can help the kind of testing whatever you design it for. My line of thinking here was that such a mode will help a developer to test a component, but in case users have also some requirements which might be fulfilled by that mode (or by certain test methods), it could be used by them, too.
    – Doc Brown
    Oct 6, 2023 at 9:40
  • oh that would be cool and I also implement such simpler components. However, currently it is a private object because I am afraid of making any components public as I would have harder time changing it in the future if someone decided to use it. Still thanks for the idea~ Oct 6, 2023 at 10:49
2

What you want to do is test the contract between the two objects.

One set of unit tests checks the behavior of the sub-object in isolation. This set of tests is essentially a representation of various usage scenarios (with respect to the contract) that can originate from your "big" object - the client of your sub-object.

Then you'd have another set of unit tests that focuses on the "big" object, or rather, its part of the contract. There, you replace the sub-object with a mock (or a spy) that, for each test case, fakes test-specific responses/behaviors of the sub-object, and you check if the "big" object makes the right calls, handles the responses properly, etc.

You'd have to redesign to inject the sub-object as a dependency (or add a new constructor). You can then provide a helper function/constructor/factory that returns a prebuilt "big" object for the user's convenience. That makes it easy to get started with the library, but the more advanced users still have the freedom to construct the object "manually", and inject dependencies of their choosing.

You can also throw in a couple of integration tests (in the sense that you don't use a mock, but the real dependency) to make sure everything works together.

1
  • Yes, I like the idea of having a separate constructor that receives the sub-object factory to also test the constructor call. I don't want to rely on integration test to test the whole behavior as it is relatively harder, but having one or two integration test makes sense for acceptance test. Thanks for your answer~ Oct 6, 2023 at 7:42

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.