java 工厂设计模式 - 不使用静态方法,因为单元测试是一个问题

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6606056/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-30 16:35:32  来源:igfitidea点击:

Factory design pattern - Not use static methods because unit testing is a problem

javadesign-patternsjunitfactory-pattern

提问by Prasanna

I know this question has been asked several times in stackoverflow but somehow still had some trouble figuring out a solution. The following example I think is a good case to have static methods

我知道这个问题在 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;
    }

    ....
    ....
}

And suppose if I have a CommunicationService like the following

假设我有一个如下的 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
}

Like this, different communication services can create and maintain multiple type of connections.

像这样,不同的通信服务可以创建和维护多种类型的连接。

I want to test initConnectionPool method and in a unit test environment, socket creation will definitely fail.

我想测试 initConnectionPool 方法,在单元测试环境中,socket 创建肯定会失败。

I can change the ConnectionFactory to a concrete class and mock it out. But isn't this case a good situation to create a class with static methods? I am not trying to maintain any state in ConnectionFactory. So, if using static methods where it may be appropriate may cause testing problems when do we use static methods? Or is it not appropriate to use static methods here?

我可以将 ConnectionFactory 更改为一个具体的类并模拟它。但是这种情况不是创建带有静态方法的类的好方法吗?我不想在 ConnectionFactory 中维护任何状态。那么,如果在合适的地方使用静态方法可能会导致测试问题,那么我们什么时候使用静态方法呢?还是在这里使用静态方法不合适?

EDIT: Solution I went with

编辑:我使用的解决方案

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
}

In the test overrode connectToHost and returned a mock.

在测试中覆盖了 connectToHost 并返回了一个模拟。

回答by Cameron Skinner

If you use a library like JMockItthen you can mock out static methods for unit testing.

如果您使用像JMockIt这样的库,那么您可以模拟用于单元测试的静态方法。

回答by bniwredyc

I think you should read this article: Static Methods are Death to Testability(Google Testing Blog).

我认为你应该阅读这篇文章:静态方法是可测试性的死亡(谷歌测试博客)。

Despite of your ConnectionFactoryclass doesn't maintain any state information, I would suggest you to create concrete class and go like this:

尽管您的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));
        ...
    }    
    ...
}

The rest of your code will not change at all, but for testing purposes you will be able to create TestConnectionFactory class:

您的其余代码根本不会更改,但出于测试目的,您将能够创建 TestConnectionFactory 类:

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

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

and use it for testing the CommunicationServicelike this:

并使用它来测试CommunicationService这样的:

CommunicationService service = new CommunicationService(new TestConnectionFactory());
// Tests
...

回答by Nrj

I think its better to use static methods over here.

我认为最好在这里使用静态方法。

In test environment or any other environment - ConnectionFactory should be initialized using different set of properties.

在测试环境或任何其他环境中 - ConnectionFactory 应该使用不同的属性集进行初始化。

So in effect you should have different set of properties (containing connectiontype, port etc) for different environments and you can init using appropriate one.

因此,实际上您应该为不同的环境设置不同的属性集(包含连接类型、端口等),并且您可以使用适当的属性进行初始化。

回答by jgauffin

I alwaysmake my singletons facades.

总是让我的单身人士立面。

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

Doing it like this makes singletons flexible and you can easily switch implementations.

这样做可以使单例变得灵活,并且您可以轻松切换实现。

回答by irreputable

As far as testing is concerned, it's easy to solve

就测试而言,很容易解决

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(...);

The testflag isn't volatile. It's thread safe in production code. Most likely if(test)will be optimized off by JVM.

test标志不volatile。它在生产代码中是线程安全的。很可能if(test)会被 JVM 优化掉。

回答by 01es

Make an immutable ConnectionFactory class initialised with a ConnectionType instance, IP address and port:

使用 ConnectionType 实例、IP 地址和端口初始化一个不可变的 ConnectionFactory 类:

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

This way method initConnectionPool would take an instance of ConnectionFactory as an argument and you'd have no problems passing whatever mock/stub/concrete implementation you need during testing and other situations.

这种方法 initConnectionPool 将 ConnectionFactory 的实例作为参数,并且在测试和其他情况下传递您需要的任何模拟/存根/具体实现都没有问题。

回答by Bohemian

A pattern that might suit you is the singleton pattern, which give you the best of both worlds: static-like behaviour with instance methods. In its simplest form, it looks like this:

一种可能适合您的模式单例模式,它为您提供两全其美的优势:具有实例方法的类静态行为。在最简单的形式中,它看起来像这样:

public class ConnectionFactory {

    private static ConnectionFactory INSTANCE = new ConnectionFactory();

    private ConnectionFactory() {} // private constructor

    public static ConnectionFactory getInstance() {
        return INSTANCE;
    }

    public void someMethod() {
        ...
    }
}

Callers use it like this:

调用者像这样使用它:

ConnectionFactory.getInstance().someMethod();

java.util.Calendar is an example of a JDK class that uses this pattern

java.util.Calendar 是使用此模式的 JDK 类的示例

回答by Adriaan Koster

First of all: I don't see any problem with your ConnectionFactory as long as it doesn't contain any state (which it looks like it doesn't). The problems you have with testing do not arise from the static factory method in this case.

首先:只要你的 ConnectionFactory 不包含任何状态(看起来它没有),我就看不到任何问题。在这种情况下,您在测试中遇到的问题不是由静态工厂方法引起的。

Unit testing code that interacts directly with environment elements like the file system, a database or the network is always hard. In this case you would need to mock socket creation to do so and that is probably not worth the effort. Even if you did it, the test code would be large, hard to maintain and brittle.

直接与文件系统、数据库或网络等环境元素交互的单元测试代码总是很困难。在这种情况下,您需要模拟套接字创建来这样做,这可能不值得付出努力。即使你这样做了,测试代码也会很大,难以维护且脆弱。

Please reconsider why you want to write a unit test for this class...

请重新考虑为什么要为此类编写单元测试...

This connection factory might be more easily covered by an integrationtest. You could set up a local socket server, record the input received and mock the output sent. Something like below:

这个连接工厂可能更容易被集成测试覆盖。您可以设置本地套接字服务器,记录收到的输入并模拟发送的输出。像下面这样:

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

Your test code might look something like below:

您的测试代码可能如下所示:

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

Hope this helps.

希望这可以帮助。

EDIT: Ok, so I slightly misunderstood your question, sorry. Above approach is still a valid solution I think. It will allow you to keep your static factory methods and run an integration test. However, your desire to unittestyour service method makes perfect sense. The solution you picked (overriding connectToHost in your service) seems pretty good to me. Another way to do it is like this:

编辑:好的,所以我稍微误解了你的问题,抱歉。我认为上述方法仍然是一个有效的解决方案。它将允许您保留静态工厂方法并运行集成测试。然而,你的愿望单元测试你的服务的方法非常有意义。您选择的解决方案(在您的服务中覆盖 connectToHost)对我来说似乎很好。另一种方法是这样的:

Create an interface to wrap the dependency:

创建一个接口来包装依赖项:

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

Wrap the call to your factory and add a setter:

将调用包装到您的工厂并添加一个 setter:

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

Call your connectionProvider instead of directly calling the factory:

调用你的 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
    // ......
    // ......
}

In your unittest, use the setter to provide a mock.

在您的单元测试中,使用 setter 提供模拟。

回答by iMysak

Another common practice is to define your own helper class which can wrap calls static methods and constructors. It is quite useful if you need to mock some third-party libraries, that you can't rewrite.

另一种常见的做法是定义您自己的辅助类,它可以包装调用静态方法和构造函数。如果您需要模拟某些无法重写的第三方库,这将非常有用。

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