Testable design for a class that can only be instantiated through a static method
https://softwareengineering.stackexchange.com/questions/356407
-
18-01-2021 - |
문제
I'm attempting to design a class that is to be instantiated through the use of a static method, something like newInstance(param1, param2)
. The reason behind this is that 2 of the 4 parameters it takes are from the same package, but I want to hide them from the public.
In order to achieve this, I made the constructor private. However, I just realize that I am now unable to pass the 2 parameters on my tests.
It goes like this
class Foo {
private constructor(p1, p2, p3, p4) {}
public static newInstance(p1, p2) {
p3 = new p3;
p4 = new p4;
return new static(p1, p2, p3, p4);
}
}
Obviously my design is wrong, but I can't think of a way to abstract the existence of p3 and p4 to other developers, while still being able to pass them as dependency in order to apply dependency inversion.
What design can I use to achieve this? Maybe I should just create a FooFactory
and document that they're supposed to use that...
I'm basically writing an SDK for an REST API. These REST API is not designed by me nor my company (so I can't make the REST API follow industry standards), and it has a very complicated authentication mechanism. It is complicated enough that I want to achieve the following:
- Break it down into little testable components, so that it's easier to maintain, and the other developers are able to work with just the endpoints the SDK expose, which encapsulate the domain model.
- Abstract the construction of the dependency graph for the SDK, so that they can just use the
CompanyClient
class and call business domain methods likegetCustomerList
. - Completely abstract the construction of the customized headers, parameters, etc... of the HTTP request.
I agree this work as a unit so it make sense just to test the CompanyAPI
class by itself; However, that would make it difficult to maintain the sub-mechanisms that are needed to make it work.
In summary, by breaking it down into smaller pieces, it's easy to test that every mechanism works according to the specification.
For this reason I want just to expose 2 parameters for the library users. They don't need to know the underlying implementation. However, I DO need to test and since the dependencies include a HTTP client, I need to inject mock objects.
해결책
If you are using c#, you can use this "friend" approach:
Add another (possibly parameterless) constructor to the class solely for your test project.
"Hide" the constructor from other developers by making it internal. You can then make the constructor visible to your test project by using the InternalsVisibleToAttribute.
[assembly:InternalsVisibleTo("MyUnitTestProject")]
class Foo {
internal constructor(p1, p2, p3, p4) {}
public static newInstance(p1, p2) {
p3 = new p3;
p4 = new p4;
return new static(p1, p2, p3, p4);
}
}
If you are not using c#, or would rather not use "friend" attributes:
Create a non-static factory for this class, along with an interface for the factory, e.g.
IFooFactory
.interface IFooFactory { Foo newInstance(p1, p2); } class FooFactory : IFooFactory { public Foo newInstance(p1, p2) { return new Foo(p1, p2, new p3(), new p4()); } }
In production code, inject the non-static factory into any class that needs to be able to instantiate a
Foo
. Use your injection container to ensure that there is only one instance of the factory per thread/process/whatever is appropriate for your application.class SomeOperation { private readonly IFooFactory _fooFactory; public SomeOperation(IFooFactory fooFactory) { _foofactory = fooFactory; } } class CompositionRoot { public RegisterDependencies(IContainer container) { container.RegisterType<IFooFactory, FooFactory>().instancePerProcess(); //Syntax may vary container.RegisterType<SomeOperation, SomeOperation>(); } }
Replace any call to
Foo.newInstance(p1, p2)
with
_fooFactory.newInstance(p1, p2);
In test code, write your own test factory that conforms to
IFooFactory
and inject it instead of the production factory. Your test factory may be able to get away with supplying dummyp3
andp4
or you can stub those too.
다른 팁
Based on what you want, it feels Foo
, along with p3
and p4
all form a unit. As such, they should be tested together as a unit. Which means instantiating Foo
through the static method is perfectly fine for unit test.
If this results in too complicated tests, then that means your decision to not be able to instantiate Foo
with different classes might not be correct one.