Reputation: 10761
I've started refactoring my code to make it testable. One area I've found a problem is where I've mixed object construction with application logic. If I have a constructor called SomeClass
which performs application logic but also instantiates another class I run in to problems when I try to test:
function SomeClass() {
//does lots of application type work
//but then also instantiates a different object
new AnotherClass();
}
Testing becomes difficult because now I need to find a way to create AnotherClass
in the test environment.
I've dealt with this problem using dependency injection. So SomeClass
takes an instance of AnotherClass
as a parameter:
function SomeClass(anotherObj) {
}
Problem with this is as far as I can see is all this does is defer the problem to somewhere else in my application. I still have to create the anotherObj
from AnotherClass
somewhere else in my code.
This google testing article http://googletesting.blogspot.co.uk/2008/08/by-miko-hevery-so-you-decided-to.html suggests:
In order to have a testable code-base your application should have two kinds of classes. The factories, these are full of the "new" operators and are responsible for building the object graph of your application, but don't do anything.And the application logic classes which are devoid of the "new" operator and are responsible for doing work.
This sounds exactly like my problem and the factory type pattern is what I need. So I tried something like:
function anotherClassFactory() {
return new AnotherClass();
}
function SomeClass() {
anotherClassFactory();
}
But then SomeClass
still has a dependency on the factory. How do I get around this correctly?
Upvotes: 2
Views: 137
Reputation: 1075169
(I'm making this a community wiki answer because I frankly think it only answers part of the question, while leaving too much unsaid. Hopefully others with more knowledge can improve it.)
But then
SomeClass
still has a dependency on the factory. How do I get around this correctly?
According to this article linked by the one you linked, you do it like this:
anotherclass.js
:
function AnotherClass() {
}
AnotherClass.prototype.foo = function() { /* ... */ };
AnotherClass.prototype.bar = function() { /* ... */ };
AnotherClass.prototype.baz = function() { /* ... */ };
someclass.js
:
function SomeClass(a) {
// ...app logic...
// Use AnotherClass instance `a`; let's say you're going to call `foo`,
// but have no use for `bar` or `baz`
a.foo();
// ...app logic...
}
someclass-test.js
:
function someClass_testSomething() {
var sc = new SomeClass({
foo: function() { /* ...appropriate `foo` code for this test... */}
});
// ...test `sc`...
}
function someClass_testSomethingElse() {
// ...
}
app.js
:
function buildApp() {
return {
// ...lots of various things, including:
sc: new SomeClass(new AnotherClass())
};
}
So the real app is built using buildApp
, which gives SomeClass
its AnotherClass
instance. Your tests for SomeClass
would use someClass_testSomething
and such, which uses the real SomeClass
but a mocked-up instance rather than a real AnotherClass
containing just enough of it for the purposes of the test.
My dependency-injection-fu is weak, though, and I frankly don't see how buildApp
scales to the real world, nor do I see what you're supposed to do if a method has to create an object to do its job, e.g.:
SomeClass.prototype.doSomething = function() {
// Here, I need an instance of AnotherClass; maybe I even need to
// create a variable number of them, depending on logic internal
// to the method.
};
You're not going to pass everything the method needs as arguments, it would be a spaghetti nightmare. This is probably why for more static languages, there are usually tools rather than just coding patterns involved.
In JavaScript, of course, we have another option: Just go ahead and use new AnotherClass
in the code:
anotherclass.js
:
function AnotherClass() {
}
AnotherClass.prototype.foo = function() { /* ... */ };
AnotherClass.prototype.bar = function() { /* ... */ };
AnotherClass.prototype.baz = function() { /* ... */ };
someclass.js
:
function SomeClass() {
// ...app logic...
// Use AnotherClass instance `a`; let's say you're going to call `foo`,
// but have no use for `bar` or `baz`
(new AnotherClass()).foo();
// ...app logic...
}
someclass-test.js
:
var AnotherClass;
function someClass_testSomething() {
// Just enough AnotherClass for this specific test; there might be others
// for other tests
AnotherClass = function() {
};
AnotherClass.prototype.foo = function() { /* ...appropriate `foo` code for this test... */};
var sc = new SomeClass();
// ...test `sc`...
}
function someClass_testSomethingElse() {
// ...
}
You use anotherclass.js
and someclass.js
in your real app, and you use someclass.js
and someclass-test.js
when testing SomeClass
.
That's a rough sketch, of course; I'm assuming your real-world app probably doesn't have globals (SomeClass
, AnotherClass
) all over the place, but however it is you're containing SomeClass
and AnotherClass
can presumably also be used to contain SomeClass
, and to contain the tests for it and their various fake AnotherClass
s.
Upvotes: 1