Правильный способ модульного тестирования класса с внутренним классом - PullRequest
4 голосов
/ 24 ноября 2011

Класс A имеет внутренний класс B. Класс A имеет закрытый список объектов класса B, который он делает доступным с помощью методов getBs, addB и removeB.Как выполнить модульное тестирование метода removeB?

Я надеялся создать два одинаковых макета класса B, добавить каждый из них, а затем удалить один из них дважды (в результате удаляются оба).Однако после неудачи я узнал, что метод equals не будет вызываться для фиктивных объектов.

Было ли глупо (или невозможно) пытаться изолировать внешний класс от его внутреннего класса для модульного тестирования?


Пример кода следует

Класс для тестирования:

import java.util.ArrayList;
import java.util.List;

public class Example {
    private List<Inner> inners = new ArrayList<Inner>();

    public List<Inner> getInners() {
        return inners;
    }

    public void addInner(Inner i) {
        inners.add(i);
    }

    public void removeInner(Inner i) {
        inners.remove(i);
    }

    /**
     * equalityField represents all fields that are used for testing equality
     */
    public class Inner {
        private int equalityField;
        private int otherFields;

        public int getEqualityField() {
            return equalityField;
        }

        public Inner(int field) {
            this.equalityField = field;
        }

        @Override
        public boolean equals(Object o) {
            if (o == null)
                return false;
            if (o.getClass() != this.getClass())
                return false;
            Inner other = (Inner) o;
            if (equalityField == other.getEqualityField())
                return true;
            return false;
        }
    }
}

Тестовый пример, который не сработал так хорошо:

import static org.junit.Assert.*;
import org.junit.Test;
import org.easymock.classextension.EasyMock;

public class ExampleTest {
    @Test
    public void testRemoveInner() {
        Example.Inner[] mockInner = new Example.Inner[2];
        mockInner[0] = EasyMock.createMock(Example.Inner.class);
        mockInner[1] = EasyMock.createMock(Example.Inner.class);        

        Example e = new Example();
        e.addInner(mockInner[0]);
        e.addInner(mockInner[1]);
        e.removeInner(mockInner[0]);
        e.removeInner(mockInner[0]);
        assertEquals(0, e.getInners().size());
    }
}

Ответы [ 4 ]

5 голосов
/ 24 ноября 2011

Почему вы издеваетесь над своим внутренним классом?Если бы я столкнулся с этой проблемой, я бы просто использовал класс как есть и проверил, что поведение внешнего класса работает как положено.Для этого не нужно издеваться над внутренним классом.

В качестве отступления: при переопределении метода equals () целесообразно также переопределить метод hashCode ().

5 голосов
/ 25 ноября 2011

Во-первых, ответ на ваш вопрос: да, вообще плохая идея пытаться разделить внутренний и внешний класс при модульном тестировании.Обычно это происходит потому, что они тесно связаны, например, Inner имеет смысл только в контексте Outer, или Outer имеет фабричный метод, который возвращает реализацию интерфейса, Inner.Если они на самом деле не связаны, то разделите их на два файла.Это облегчает вам тестирование.

Во-вторых, (используя приведенный выше код в качестве примера) вам не нужно издеваться над приведенным выше кодом.Просто создайте несколько экземпляров и все готово.Похоже, у вас достаточно для работы.Вы всегда можете сделать что-то вроде:

public void testRemoveInner() {
    Example.Inner[] inner = new Example.Inner(45);

    Example e = new Example();
    e.addInner(inner);
    e.addInner(inner);
    e.removeInner(inner);
    assertEquals(0, e.getInners().size());
}

Не нужно издеваться.

В-третьих, попытайтесь выяснить, что вы на самом деле тестируете.В приведенном выше коде вы проверяете, что если я добавлю что-то в список, я смогу удалить это.Кстати, вы утверждаете, что если есть несколько объектов, которые «равны»;Приведенный выше код этого не делает из определения Collection # remove () :

Удаляет один экземпляр указанного элемента из этой коллекции, если он присутствует(необязательная операция).Более формально, удаляет элемент e такой, что (o == null? E == null: o.equals (e)), если эта коллекция содержит один или несколько таких элементов.

Это действительночто вы хотите проверить?

В-четвертых, если вы реализуете equals, также реализует hashCode (см. Переопределение equals и hashCode в Java ).

2 голосов
/ 28 января 2017

Как новичок в TDD, который только что столкнулся с этой ситуацией в первый раз, я обнаружил, что я получил «непроверяемые» внутренние классы в результате рефакторинга, и если я использую «чистый» подход TDD, мне интересно, если вы может закончить с внутренними классами любым другим способом.

Проблема в том, что, если одна или несколько ссылок сделаны на объект внешнего класса из внутреннего класса, этот конкретный рефакторинг часто нарушает один или несколько тестов. Причина этого довольно проста: ваш фиктивный объект, если spy, на самом деле является оберткой вокруг реального объекта

MyClass myClass = spy( new MyClass() );

... но внутренние классы всегда будут ссылаться на реальный объект, поэтому часто бывает так, что попытка применить mock к myClass не будет работать. Хуже того, даже без насмешек существует высокая вероятность того, что вещь рухнет совершенно и необъяснимым образом, просто если она будет заниматься своим обычным делом. Также помните, что ваш шпион не будет запускать настоящий метод конструктора для себя: многое может пойти не так.

Учитывая, что наша разработка наших тестов является инвестицией в качество, мне кажется, что было бы ужасно стыдно просто сказать: «Хорошо, я просто собираюсь отказаться от этого теста».

Я предлагаю два варианта:

  1. если вы замените прямой доступ вашего внутреннего класса к полям внешнего класса методами получения / установки (которые могут быть private, как ни странно), это будет означать, что это методы mock , которые будут использоваться ... и поля mock. Ваши существующие тесты должны затем пройти успешно.

  2. другая возможность состоит в том, чтобы реорганизовать этот внутренний класс, чтобы сделать его автономным классом, экземпляр которого заменяет ваш внутренний класс, и перенести один или несколько тестовых методов в новый тестовый класс для этого нового класса. Затем вы столкнетесь с (простой) задачей подстроить вещи так, чтобы ссылки на объект внешнего класса были параметризованы (т.е. в 99% случаев переданы как параметр конструктора), а затем могли быть соответствующим образом смоделированы. Это не должно быть слишком сложно. Хотя вам может потребоваться добавить подходящие методы получения / установки для полей private во внешнем классе и / или создать один или несколько его private методов package-private. С этого момента внутренний класс становится «черным ящиком» для тестирования внешнего класса.

При использовании любого из этих подходов вы не понесли потери качества.

0 голосов
/ 24 ноября 2011

Вы можете расширить тестируемый класс (A) с помощью специального класса ATest, который предлагает открытый метод, позволяющий вам заглянуть в закрытый список B's

...