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

Я знаю, что этот вопрос задавался несколько раз в 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, а в среде unit test создание сокетов, безусловно, не удастся.

Я могу изменить 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
}

В тесте overrode connectToHost и вернул макет.

Ответ 1

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

Ответ 2

Я думаю, что вы должны прочитать эту статью: Статические методы - это смерть для тестирования (Блог тестирования 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

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

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

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

Ответ 4

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

public interface IConnectionFactory //i know that it 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;
    }
}

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

Ответ 5

Создайте неизменяемый класс 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 в качестве аргумента, и у вас не возникнет проблем с передачей какой-либо макетной/нужной/конкретной реализации в ходе тестирования и в других ситуациях.

Ответ 6

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

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. Он защищен потоком в производственном коде. Скорее всего, if(test) будет оптимизирован JVM.

Ответ 7

Образец, который может вам подойдет, - это 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, который использует этот шаблон

Ответ 8

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

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

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

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

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 API
connection.open();
String receivedOutput = connection.send(sentInput);
connection.close();

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

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

EDIT: Хорошо, поэтому я немного неправильно понял ваш вопрос, извините. Я думаю, что выше подход по-прежнему остается верным решением. Это позволит вам сохранить ваши статические методы factory и запустить тест интеграции. Однако ваше желание унифицировать ваш метод обслуживания имеет смысл. Решение, которое вы выбрали (переопределение connectToHost в вашем сервисе), кажется мне очень хорошим. Другой способ сделать это:

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

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

Оберните вызов на factory и добавьте установщик:

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;
}

Вызовите ваш connectionProvider вместо прямого вызова factory:

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
    // ......
    // ......
}

В своем unittest, используйте setter для создания макета.

Ответ 9

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

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);
        }
    }
}