Шаблон фабричного проектирования - не использовать статические методы, потому что проблема заключается в модульном тестировании - PullRequest
14 голосов
/ 07 июля 2011

Я знаю, что этот вопрос задавался несколько раз в stackoverflow, но каким-то образом все еще возникали проблемы с поиском решения.Следующий пример, на мой взгляд, является хорошим примером для использования статических методов

public class ConnectionFactory
{
     public static Connection createConnection(ConnectionType connectionType, String ipAddr, Integer port)
    {
           //Some error checking
         switch(connectionType)
         {   
             case TCP:
                  return createTcpConnection(ipAddr, port);
             case UDP:
                  return createUdpConnection(ipAddr, port);
             case RTP:
                  return createRtpConnection(ipAddr, port);
             case SCTP:
                  return createRtpConnection(ipAddr, port);
             default:
                  break;
         }
    }

    // TcpConnection, RtpConnection, SctpConnection and UdpConnection implement interface Connection
    public Connection createTcpConnection()
    {
        Connection connection = new TcpConnection();
         .....
         .....
         return connection;
    }

    public Connection createUdpConnection()
    {
        Connection connection = new UdpConnection();
        .....
        .....
        return connection;
    }

    ....
    ....
}

И предположим, что если у меня есть CommunicationService, подобный следующему

public class CommunicationService
{
    public void initConnectionPool(ConnectionType connectionType)
    {
        for(int i = 0; i < MAX_CONNECTIONS; i++)
             connectionList.add(ConnectionFactory.createConnection(connectionType, "domain.com", 40203));

        //Some more code here to do further processing
          ......
          ......
    }    

    //Some more methods
}

Таким образом, различные службы связи могут создаватьподдерживать несколько типов соединений.

Я хочу протестировать метод initConnectionPool, и в среде модульного тестирования создание сокетов обязательно завершится неудачей.

Я могу изменить ConnectionFactory на конкретный класс и макетировать его.Но разве это не хорошая ситуация для создания класса со статическими методами?Я не пытаюсь поддерживать любое состояние в ConnectionFactory.Итак, если использование статических методов там, где это возможно, может вызвать проблемы с тестированием, когда мы используем статические методы?Или здесь неуместно использовать статические методы?

РЕДАКТИРОВАТЬ: Решение Я пошел с

public class CommunicationService
{
    public void initConnectionPool(ConnectionType connectionType)
    {
        for(int i = 0; i < MAX_CONNECTIONS; i++)
             connectionList.add(connectToHost(connectionType));

        //Some more code here to do further processing
          ......
          ......
    }    

    public Connection connectToHost(ConnectionType connectionType)
    {
        ConnectionFactory.createConnection(connectionType, "domain.com", 40203)
    }
    //Some more methods
}

В тесте переопределил connectToHost и вернул ложный.*

Ответы [ 9 ]

3 голосов
/ 07 июля 2011

Я думаю, вам следует прочитать эту статью: Статические методы - это смерть для тестируемости (блог тестирования Google).

Несмотря на то, что ваш ConnectionFactory класс не содержит никакой информации о состоянии,Я бы посоветовал вам создать конкретный класс и сделать так:

public class ConnectionFactory
{
    public Connection createConnection(ConnectionType connectionType, String ipAddr, Integer port)
    {
         //Some error checking
         switch(connectionType)
         {   
             case TCP:
                  return createTcpConnection(ipAddr, port);
             case UDP:
                  return createUdpConnection(ipAddr, port);
             case RTP:
                  return createRtpConnection(ipAddr, port);
             case SCTP:
                  return createRtpConnection(ipAddr, port);
             default:
                  break;
         }
    }

    // TcpConnection, RtpConnection, SctpConnection and UdpConnection implement interface Connection
    public Connection createTcpConnection()
    {
        Connection connection = new TcpConnection();
        ...
        return connection;
    }

    public Connection createUdpConnection()
    {
        Connection connection = new UdpConnection();
        ...
        return connection;
    }
    ...    
}

public class CommunicationService
{
    private ConnectionFactory connectionFactory;

    public CommunicationService()
    {
        this(new ConnectionFactory());
    }

    public CommunicationService(ConnectionFactory factory)
    {
        connectionFactory = factory;
    }

    public void initConnectionPool(ConnectionType connectionType)
    {
        for(int i = 0; i < MAX_CONNECTIONS; i++)
             connectionList.add(connectionFactory.createConnection(connectionType, "domain.com", 40203));
        ...
    }    
    ...
}

Остальная часть вашего кода не изменится совсем, но для целей тестирования вы сможете создать класс TestConnectionFactory:

public class TestConnectionFactory : ConnectionFactory
{
    public override Connection createTcpConnection()
    {
        ...
        return testTcpConnection;
    }

    public override Connection createUdpConnection()
    {
        ...
        return testUdpConnection;
    }
}

и используйте его для тестирования CommunicationService следующим образом:

CommunicationService service = new CommunicationService(new TestConnectionFactory());
// Tests
...
3 голосов
/ 07 июля 2011

Если вы используете библиотеку типа JMockIt , тогда вы можете макетировать статические методы для модульного тестирования.

1 голос
/ 07 июля 2011

I всегда делают мои фасады синглетонов.

public interface IConnectionFactory //i know that it's not a correct java naming
{
     Connection createConnection(ConnectionType connectionType, String ipAddr, Integer port);
}

public class ConnectionFactory
{
    private IConnectionFactory _implementation;

    public static Connection createConnection(ConnectionType connectionType, String ipAddr, Integer port)
    {
        return _implementation.createConnection(connectionType, ipAdd, port);
    }

    //DO assign a factory before doing anything else.
    public static void AssignFactory(IConnectionFactory implementation)
    {
         _implementation = implementation;
    }
}

Делая так, вы делаете гибкие синглтоны и вы можете легко переключать реализации.

1 голос
/ 07 июля 2011

Я думаю, что здесь лучше использовать статические методы.

В тестовой среде или любой другой среде - ConnectionFactory должен быть инициализирован с использованием другого набора свойств.

Таким образом, в действительности вы должны иметьразличный набор свойств (содержащий тип соединения, порт и т. д.) для разных сред, и вы можете инициировать его, используя соответствующее.

0 голосов
/ 04 октября 2017

Другой распространенной практикой является определение собственного вспомогательного класса, который может переносить вызовы статических методов и конструкторов. Это очень полезно, если вам нужно смоделировать некоторые сторонние библиотеки, которые вы не можете переписать.

public class CommunicationService {
    private final CommunicationServiceHelper helper;
    public CommunicationService() {
        this(new CommunicationServiceHelper());
    }

    @VisibleForTesting
    CommunicationService(CommunicationServiceHelper helper) {
        this.helper = helper;
    }

    public void initConnectionPool(ConnectionType connectionType)
    {
        for(int i = 0; i < MAX_CONNECTIONS; i++)
             connectionList.add(helper.createConnection(connectionType, "domain.com", 40203));

        //Some more code here to do further processing
          ......
          ......
    }    

    //Some more methods

    @VisibleForTesting
    static class CommunicationServiceHelper {
        public Connection createConnection(ConnectionType connectionType, String ipAddr, Integer port) {
            return ConnectionFactory.createConnection(connectionType, ipAddr, port);
        }
    }
}
0 голосов
/ 07 июля 2011

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

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

Пожалуйста, пересмотрите, почему вы хотите написать модульный тест для этого класса ...

Эта фабрика соединений может быть легче охвачена тестом интеграция . Вы можете настроить локальный сервер сокетов, записать полученный ввод и смоделировать отправленный вывод. Примерно так:

public class TestServer {

    private int port;
    private String[] responses;
    private List<String> inputLines = new ArrayList<String>();

    public TestServer(int port, String ... responses) {
        this.port = port;
        this.responses = responses;
    }

    public void run() throws IOException {

        ServerSocket serverSocket = new ServerSocket(port);
        Socket clientSocket = serverSocket.accept();
        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
        BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String inputLine;
        int i = 0;
        while ((inputLine = in.readLine()) != null) {
            inputLines.add(inputLine);
            out.println(responses[i++]);
        }
        out.close();
        in.close();
        clientSocket.close();
        serverSocket.close();
    }

    public List<String> getInputLines() {
        return inputLines;
    }
}

Ваш тестовый код может выглядеть примерно так:

// setup
String sentInput = "hello";
String sentOutput = "hi there!";
int port = 4444;
TestServer server = new TestServer(port, sentOutput);
server.run();

// test 
Connection connection = ConnectionFactory.createConnection(ConnectionType.TCP, "localhost", port);
// not sure of your Connection's API
connection.open();
String receivedOutput = connection.send(sentInput);
connection.close();

// verify
List<String> inputLines = server.getInputLines();
assertEquals(sentInput , inputLines.get(0)); 
assertEquals(sentOutput, receivedOutput);

Надеюсь, это поможет.

РЕДАКТИРОВАТЬ : Хорошо, поэтому я немного неправильно понял ваш вопрос, извините. Вышеупомянутый подход все еще является верным решением, я думаю. Это позволит вам сохранить ваши статические фабричные методы и запустить интеграционный тест. Тем не менее, ваше желание unittest ваш метод обслуживания имеет смысл. Решение, которое вы выбрали (переопределяя connectToHost в вашем сервисе), кажется мне довольно хорошим. Еще один способ сделать это так:

Создать интерфейс для переноса зависимости:

public interface ConnectionProvider {
    Connection provideConnection(ConnectionType connectionType);
}

Завершите вызов на заводе и добавьте сеттер:

private ConnectionProvider connectionProvider = new ConnectionProvider() {
    public Connection provideConnection(ConnectionType connectionType) {
        return ConnectionFactory.createConnection(connectionType, "domain.com", 40203);
    }
};

public void setConnectionProvider(ConnectionProvider connectionProvider) {
    this.connectionProvider = connectionProvider;
}

Позвоните своему провайдеру соединения вместо прямого вызова фабрики:

public void initConnectionPool(ConnectionType connectionType) {
    for (int i = 0; i < MAX_CONNECTIONS; i++) {
        connectionList.add(connectionProvider.provideConnection(connectionType));
    }
    // Some more code here to do further processing
    // ......
    // ......
}

В вашем юнит-тесте используйте сеттер, чтобы создать макет.

0 голосов
/ 07 июля 2011

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

public class ConnectionFactory {

    private static ConnectionFactory INSTANCE = new ConnectionFactory();

    private ConnectionFactory() {} // private constructor

    public static ConnectionFactory getInstance() {
        return INSTANCE;
    }

    public void someMethod() {
        ...
    }
}

Вызывающие используют его так:

ConnectionFactory.getInstance().someMethod();

java.util.Calendar - пример класса JDK, который использует этот шаблон

0 голосов
/ 07 июля 2011

Что касается тестирования, его легко решить

public class ConnectionFactory

    static boolean test = false;

    public static Connection createConnection(ConnectionType connectionType, String ipAddr, Integer port)

        if(test) ...
        else ...


public class ConnectionFactoryTest // in the same package
    public static void enableTest(){ ConnectionFactory.test=true; }

// in some other test classes
    ConnectionFactoryTest.enableTest();
    ConnectionFactory.createConnection(...);

Флаг test не volatile. Это потокобезопасно в производственном коде. Скорее всего, JVM оптимизирует if(test).

0 голосов
/ 07 июля 2011

Создайте неизменный класс ConnectionFactory, инициализированный экземпляром ConnectionType, IP-адресом и портом:

public class ConnectionFactory {
     private final ConnectionType connectionType;
     private final String ipAddr;
     private final Integer port;

     public ConnectionFactory(final ConnectionType connectionType) {
          this.connectionType = connectionType;
          this.ipAddr = ipAddr;
          this.port = port;
     }

     public Connection createConnection() {
        // TODO your code here
        ...  
     }
     ....
}

Таким образом, метод initConnectionPool будет принимать экземпляр ConnectionFactory в качестве аргумента, и у вас не будет проблем с передачей любогореализация макета / заглушки / бетона, необходимая вам во время тестирования и других ситуациях.

...