#javascript #unit-testing #testing
#javascript #модульное тестирование #тестирование
Вопрос:
Я начал рефакторинг своего кода, чтобы сделать его тестируемым. Одна из областей, в которой я обнаружил проблему, заключается в том, что я смешал построение объекта с логикой приложения. Если у меня вызывается конструктор SomeClass
, который выполняет логику приложения, но также создает экземпляр другого класса, я сталкиваюсь с проблемами при попытке тестирования:
function SomeClass() {
//does lots of application type work
//but then also instantiates a different object
new AnotherClass();
}
Тестирование становится сложным, потому что теперь мне нужно найти способ создания AnotherClass
в тестовой среде.
Я разобрался с этой проблемой, используя внедрение зависимостей. So SomeClass
принимает экземпляр AnotherClass
в качестве параметра:
function SomeClass(anotherObj) {
}
Проблема с этим, насколько я вижу, заключается в том, что все, что это делает, это переносит проблему куда-то еще в моем приложении. Мне все еще нужно создать объект anotherObj
из AnotherClass
другого места в моем коде.
Эта статья о тестировании Google http://googletesting.blogspot.co.uk/2008/08/by-miko-hevery-so-you-decided-to.html предполагает:
Чтобы иметь тестируемую кодовую базу, ваше приложение должно иметь два вида классов. Фабрики, они полны «новых» операторов и отвечают за построение графа объектов вашего приложения, но ничего не делают.И классы логики приложения, которые лишены оператора «new» и отвечают за выполнение работы.
Это звучит точно так же, как моя проблема, и шаблон заводского типа — это то, что мне нужно. Итак, я попробовал что-то вроде:
function anotherClassFactory() {
return new AnotherClass();
}
function SomeClass() {
anotherClassFactory();
}
Но тогда SomeClass
все еще зависит от фабрики. Как мне правильно обойти это?
Ответ №1:
(Я делаю это ответом сообщества wiki, потому что, честно говоря, думаю, что он отвечает только на часть вопроса, оставляя слишком много недосказанного. Надеюсь, другие, обладающие большими знаниями, смогут улучшить его.)
Но тогда
SomeClass
все еще зависит от фабрики. Как мне правильно обойти это?
Согласно этой статье, на которую вы ссылаетесь, вы делаете это следующим образом:
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())
};
}
Таким образом, реальное приложение создается с использованием buildApp
, которое предоставляет SomeClass
его AnotherClass
экземпляр. Ваши тесты для SomeClass
будут использовать someClass_testSomething
и тому подобное, в котором используется реальный SomeClass
, но макет экземпляра, а не реальный AnotherClass
, содержащий его ровно столько, сколько нужно для целей теста.
Однако мой dependency-injection-fu слаб, и я, честно говоря, не вижу, как buildApp
масштабируется в реальном мире, и я не вижу, что вы должны делать, если метод должен создать объект для выполнения своей работы, например:
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.
};
Вы не собираетесь передавать все, что нужно методу в качестве аргументов, это был бы кошмар спагетти. Вероятно, поэтому для более статических языков обычно используются инструменты, а не просто шаблоны кодирования.
В JavaScript, конечно, у нас есть другой вариант: просто продолжайте и используйте new AnotherClass
в коде:
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() {
// ...
}
Вы используете anotherclass.js
and someclass.js
в своем реальном приложении, и вы используете someclass.js
and someclass-test.js
при тестировании SomeClass
.
Это, конечно, грубый набросок; Я предполагаю, что ваше реальное приложение, вероятно, не имеет глобалов ( SomeClass
, AnotherClass
) повсюду, но, тем не менее, вы его содержите SomeClass
и AnotherClass
, предположительно, также можете использовать для содержания SomeClass
и для содержания тестов для него и их различных поддельных AnotherClass
файлов.
Комментарии:
1. Спасибо за подробный ответ. Я прочитал связанную статью, и я думаю, что первая часть вашего ответа объясняет, почему я этого не сделал approach…in в реальном мире, или, по крайней мере, в реальном мире, когда дело доходит до js, я не собираюсь этого делать. На данный момент у меня есть сценарий, который вы предложили во второй части вашего ответа… который заключается в том, чтобы высмеять поддельный AnotherClass. Как вы сказали, надеюсь, кто-нибудь может предложить улучшение этого.