如何使用 Spock 对 Java 套接字 server/client 对进行单元测试?
How to unit-test Java socket server/client pair using Spock?
我正在尝试使用 Spock 为套接字客户端和服务器编写单元测试。我应该如何在我的单元测试中设置 server/client 对才能使其正常工作?
我通过在测试之外手动 运行 连接我的服务器 class 成功测试了我的客户端 class,但是当我尝试在测试中初始化它时 class,所有测试似乎都挂起,或者连接被拒绝。
目前我只使用 EchoServer and EchoClient code from Oracle.
的略微修改版本
客户class:
public class EchoClient {
private Socket echoSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String hostName, int portNumber) throws IOException {
try {
echoSocket = new Socket(hostName, portNumber);
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));
} catch (UnknownHostException e) {
System.err.printf("Don't know about host %s%n", hostName);
System.exit(1);
} catch (IOException e) {
System.err.printf("Couldn't get I/O for the connection to %s%n", hostName);
System.exit(1);
}
}
public String sendMessage(String msg) throws IOException {
out.println(msg);
return in.readLine();
}
}
服务器启动方式:
public void start(int portNumber) throws IOException {
try (
ServerSocket serverSocket =
new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println(inputLine);
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port "
+ portNumber + " or listening for a connection");
System.out.println(e.getMessage());
}
}
Spock 测试:
class EchoClientTest extends Specification {
def "Server should echo message from Client"() {
when:
EchoServer server = new EchoServer()
server.start(4444)
EchoClient client = new EchoClient()
client.startConnection("localhost", 4444)
then:
client.sendMessage("echo") == "echo"
}
}
如果我 运行 服务器与测试分开,并注释掉 Spock 测试 'when:' 中的前两行,测试将 运行 成功。但是,如果不挂起测试,我就无法达到 运行。
我应该补充一点,我已经研究过 using this guide for Stubbing and Mocking in Java with the Spock Testing Framework 的模拟,但我以前没有模拟的经验,所以我尝试使用它的任何尝试都没有成功,或者不知道是否在这种特殊情况下完全可以使用模拟。
代码的问题是 echoServer.start()
永远不会 returns 因为它停留在 while 循环中。因此我会在另一个线程中启动 EchoServer。
class EchoClientTest extends Specification {
@Shared
Thread echoServerThread
void setupSpec() {
echoServerThread = Thread.start {
EchoServer server = new EchoServer()
server.start(4444)
}
}
void cleanupSpec() {
echoServerThread.stop()
}
def "Server should echo message from Client"() {
when:
EchoClient client = new EchoClient()
client.startConnection("localhost", 4444)
then:
client.sendMessage("echo") == "echo"
}
}
请记住,Thread.stop()
已弃用。我只是用它来缩短样本。参见 https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
masooh 提供的解决方案有效,但前提是您只启动一个客户端。如果要运行多个测试用例,需要注意只调用startConnection(..)
一次,否则测试会再次挂起。这是解决集成测试问题的方法:
package de.scrum_master.Whosebug.q55475971
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientIT extends Specification {
static final int SERVER_PORT = 4444
static Thread echoServerThread
static EchoClient echoClient
void setupSpec() {
echoServerThread = Thread.start {
new EchoServer().start(SERVER_PORT)
}
echoClient = new EchoClient()
echoClient.startConnection("localhost", SERVER_PORT)
}
void cleanupSpec() {
echoServerThread?.stop()
}
@Unroll
def "server echoes client message '#message'"() {
expect:
echoClient.sendMessage(message) == message.toString()
where:
message << ["echo", "Hello world!", null]
}
}
您需要手动停止服务器线程,也无法有序地关闭客户端连接,这些都是您的应用程序代码中的问题。您应该通过为客户端和服务器提供 close/shutdown 方法来解决这些问题。
如果您想对代码进行单元测试,情况会变得更糟:
- 无法将套接字依赖项注入客户端,因为它是自己创建的,无法从外部访问它并提供用于测试的模拟。
- 如果您想用单元测试覆盖客户端的异常处理部分并检查正确的行为,您还会注意到从客户端内部的方法调用
System.exit(..)
是一个非常糟糕的主意,因为它会当它第一次击中该部分时也会中断测试。我知道您从 Oracle 示例中复制了您的代码,但它在静态 main(..)
方法中使用,即仅适用于独立应用程序的情况。可以使用它,但是在将其重构为更通用的客户端后就不能再使用了 class.
- 一个更普遍的问题是,即使在您的客户端中注释掉
System.exit(..)
,这种情况下的异常处理只会在控制台上打印一些内容,但会抑制发生的异常,因此客户端的用户 class 没有简单的方法来发现发生了什么不好的事情并处理这种情况。由于某种原因无法建立连接,她将只剩下一个不工作的客户端。您仍然可以调用 sendMessage(..)
,但随后会发生错误。
- 还有更多的问题我就不在这里说了,因为太详细了。
所以您想重构您的代码,使其更易于维护,也更易于测试。这是测试驱动开发真正有用的地方。它是一种设计工具,主要不是质量管理工具。
这个怎么样?我仍然对此不满意,但它显示了如何更容易地测试代码:
回显服务器:
现在的服务器
- 负责在单独的线程中侦听服务器端口 4444。不再需要在测试中启动额外的线程
- 为每个传入连接生成另一个新线程
- 可以同时处理多个连接(另见下面相应的集成测试)
- 有一个
close()
方法并实现了 AutoCloseable
,即可以手动关闭或通过 try-with-resources 关闭。
我还添加了一些日志记录,主要用于演示目的,因为如果测试通过,通常不会记录任何内容。
package de.scrum_master.Whosebug.q55475971;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer implements AutoCloseable {
private ServerSocket serverSocket;
public EchoServer(int portNumber) throws IOException {
this(new ServerSocket(portNumber));
}
public EchoServer(ServerSocket serverSocket) throws IOException {
this.serverSocket = serverSocket;
listen();
System.out.printf("%-25s - Echo server started%n", Thread.currentThread());
}
private void listen() {
Runnable listenLoop = () -> {
System.out.printf("%-25s - Starting echo server listening loop%n", Thread.currentThread());
while (true) {
try {
echo(serverSocket.accept());
} catch (IOException e) {
System.out.printf("%-25s - Stopping echo server listening loop%n", Thread.currentThread());
break;
}
}
};
new Thread(listenLoop).start();
}
private void echo(Socket clientSocket) {
Runnable echoLoop = () -> {
System.out.printf("%-25s - Starting echo server echoing loop%n", Thread.currentThread());
try (
Socket socket = clientSocket;
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println(inputLine);
System.out.printf("%-25s - Echoing back message: %s%n", Thread.currentThread(), inputLine);
}
System.out.printf("%-25s - Stopping echo server echoing loop%n", Thread.currentThread());
} catch (IOException e) {
e.printStackTrace();
}
};
new Thread(echoLoop).start();
}
@Override
public void close() throws Exception {
System.out.printf("%-25s - Shutting down echo server%n", Thread.currentThread());
if (serverSocket != null) serverSocket.close();
}
}
回声客户端:
现在的客户
- 不再吞下异常,而是让它们发生并由用户处理
- 可以通过其构造函数之一注入一个
Socket
实例,这允许轻松模拟并使 class 更可测试
- 有一个
close()
方法并实现了 AutoCloseable
,即可以手动关闭或通过 try-with-resources 关闭。
我还添加了一些日志记录,主要用于演示目的,因为如果测试通过,通常不会记录任何内容。
package de.scrum_master.Whosebug.q55475971;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class EchoClient implements AutoCloseable {
private Socket echoSocket;
private PrintWriter out;
private BufferedReader in;
public EchoClient(String hostName, int portNumber) throws IOException {
this(new Socket(hostName, portNumber));
}
public EchoClient(Socket echoSocket) throws IOException {
this.echoSocket = echoSocket;
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));
System.out.printf("%-25s - Echo client started%n", Thread.currentThread());
}
public String sendMessage(String msg) throws IOException {
System.out.printf("%-25s - Sending message: %s%n", Thread.currentThread(), msg);
out.println(msg);
return in.readLine();
}
@Override
public void close() throws Exception {
System.out.printf("%-25s - Shutting down echo client%n", Thread.currentThread());
if (out != null) out.close();
if (in != null) in.close();
if (echoSocket != null) echoSocket.close();
}
}
集成测试:
这类似于您自己和 masooh 的解决方案,但使用更新的客户端和服务器 classes。您会看到现在客户端和服务器的可测试性是多么容易。实际上测试的目的是只测试客户端,使用服务器只是因为它是一个集成测试。但是因为 classes 的代码结构现在更加线性,IT 实际上为客户端和服务器创建了 100% 的行覆盖率。
package de.scrum_master.Whosebug.q55475971
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientIT extends Specification {
static final int SERVER_PORT = 4444
@Shared
EchoClient echoClient
@Shared
EchoServer echoServer
void setupSpec() {
echoServer = new EchoServer(SERVER_PORT)
echoClient = new EchoClient("localhost", SERVER_PORT)
}
void cleanupSpec() {
echoClient?.close()
echoServer?.close()
}
@Unroll
def "server echoes client message '#message'"() {
expect:
echoClient.sendMessage(message) == message.toString()
where:
message << ["echo", "Hello world!", null]
}
def "multiple echo clients"() {
given:
def echoClients = [
new EchoClient("localhost", SERVER_PORT),
new EchoClient("localhost", SERVER_PORT),
new EchoClient("localhost", SERVER_PORT)
]
expect:
echoClients.each {
assert it.sendMessage("foo") == "foo"
}
echoClients.each {
assert it.sendMessage("bar") == "bar"
}
cleanup:
echoClients.each { it.close() }
}
@Unroll
def "client creation fails with #exceptionType.simpleName when using illegal #connectionInfo"() {
when:
new EchoClient(hostName, portNumber)
then:
thrown exceptionType
where:
connectionInfo | hostName | portNumber | exceptionType
"host name" | "does.not.exist" | SERVER_PORT | UnknownHostException
"port number" | "localhost" | SERVER_PORT + 1 | IOException
}
}
单元测试:
我把这个留到最后,因为你原来的问题是关于嘲笑的。所以现在我将向您展示如何通过构造函数创建模拟套接字(或更准确地说是存根)并将其注入到您的客户端中。 IE。单元测试不打开任何真正的端口或套接字,它甚至不使用服务器 class。它实际上只对客户端进行单元测试 class。连抛出的异常都经过测试
顺便说一句,stub 有点复杂,实际上表现得像 echo 服务器。我通过管道流做到了这一点。当然,也可以创建一个更简单的 mock/stub,它只是 returns 固定结果。
package de.scrum_master.Whosebug.q55475971
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientTest extends Specification {
@Unroll
def "server echoes client message '#message'"() {
given:
def outputStream = new PipedOutputStream()
def inputStream = new PipedInputStream(outputStream)
def echoClient = new EchoClient(
Stub(Socket) {
getOutputStream() >> outputStream
getInputStream() >> inputStream
}
)
expect:
echoClient.sendMessage(message) == message.toString()
cleanup:
echoClient.close()
where:
message << ["echo", "Hello world!", null]
}
def "client creation fails for unreadable socket streams"() {
when:
new EchoClient(
Stub(Socket) {
getOutputStream() >> { throw new IOException("cannot read output stream") }
getInputStream() >> { throw new IOException("cannot read input stream") }
}
)
then:
thrown IOException
}
def "client creation fails for unknown host name"() {
when:
new EchoClient("does.not.exist", 4444)
then:
thrown IOException
}
}
P.S.: 你可以为服务器编写一个类似的单元测试,而不是使用客户端 class 或真正的套接字,但我已经准备好让你自己想办法了服务器 class 也通过构造函数注入接受套接字。但是您会注意到,使用模拟测试服务器的 echo()
方法并不那么容易,所以也许您想在那里进行更多重构。
我正在尝试使用 Spock 为套接字客户端和服务器编写单元测试。我应该如何在我的单元测试中设置 server/client 对才能使其正常工作?
我通过在测试之外手动 运行 连接我的服务器 class 成功测试了我的客户端 class,但是当我尝试在测试中初始化它时 class,所有测试似乎都挂起,或者连接被拒绝。
目前我只使用 EchoServer and EchoClient code from Oracle.
的略微修改版本客户class:
public class EchoClient {
private Socket echoSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String hostName, int portNumber) throws IOException {
try {
echoSocket = new Socket(hostName, portNumber);
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));
} catch (UnknownHostException e) {
System.err.printf("Don't know about host %s%n", hostName);
System.exit(1);
} catch (IOException e) {
System.err.printf("Couldn't get I/O for the connection to %s%n", hostName);
System.exit(1);
}
}
public String sendMessage(String msg) throws IOException {
out.println(msg);
return in.readLine();
}
}
服务器启动方式:
public void start(int portNumber) throws IOException {
try (
ServerSocket serverSocket =
new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println(inputLine);
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port "
+ portNumber + " or listening for a connection");
System.out.println(e.getMessage());
}
}
Spock 测试:
class EchoClientTest extends Specification {
def "Server should echo message from Client"() {
when:
EchoServer server = new EchoServer()
server.start(4444)
EchoClient client = new EchoClient()
client.startConnection("localhost", 4444)
then:
client.sendMessage("echo") == "echo"
}
}
如果我 运行 服务器与测试分开,并注释掉 Spock 测试 'when:' 中的前两行,测试将 运行 成功。但是,如果不挂起测试,我就无法达到 运行。
我应该补充一点,我已经研究过 using this guide for Stubbing and Mocking in Java with the Spock Testing Framework 的模拟,但我以前没有模拟的经验,所以我尝试使用它的任何尝试都没有成功,或者不知道是否在这种特殊情况下完全可以使用模拟。
代码的问题是 echoServer.start()
永远不会 returns 因为它停留在 while 循环中。因此我会在另一个线程中启动 EchoServer。
class EchoClientTest extends Specification {
@Shared
Thread echoServerThread
void setupSpec() {
echoServerThread = Thread.start {
EchoServer server = new EchoServer()
server.start(4444)
}
}
void cleanupSpec() {
echoServerThread.stop()
}
def "Server should echo message from Client"() {
when:
EchoClient client = new EchoClient()
client.startConnection("localhost", 4444)
then:
client.sendMessage("echo") == "echo"
}
}
请记住,Thread.stop()
已弃用。我只是用它来缩短样本。参见 https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
masooh 提供的解决方案有效,但前提是您只启动一个客户端。如果要运行多个测试用例,需要注意只调用startConnection(..)
一次,否则测试会再次挂起。这是解决集成测试问题的方法:
package de.scrum_master.Whosebug.q55475971
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientIT extends Specification {
static final int SERVER_PORT = 4444
static Thread echoServerThread
static EchoClient echoClient
void setupSpec() {
echoServerThread = Thread.start {
new EchoServer().start(SERVER_PORT)
}
echoClient = new EchoClient()
echoClient.startConnection("localhost", SERVER_PORT)
}
void cleanupSpec() {
echoServerThread?.stop()
}
@Unroll
def "server echoes client message '#message'"() {
expect:
echoClient.sendMessage(message) == message.toString()
where:
message << ["echo", "Hello world!", null]
}
}
您需要手动停止服务器线程,也无法有序地关闭客户端连接,这些都是您的应用程序代码中的问题。您应该通过为客户端和服务器提供 close/shutdown 方法来解决这些问题。
如果您想对代码进行单元测试,情况会变得更糟:
- 无法将套接字依赖项注入客户端,因为它是自己创建的,无法从外部访问它并提供用于测试的模拟。
- 如果您想用单元测试覆盖客户端的异常处理部分并检查正确的行为,您还会注意到从客户端内部的方法调用
System.exit(..)
是一个非常糟糕的主意,因为它会当它第一次击中该部分时也会中断测试。我知道您从 Oracle 示例中复制了您的代码,但它在静态main(..)
方法中使用,即仅适用于独立应用程序的情况。可以使用它,但是在将其重构为更通用的客户端后就不能再使用了 class. - 一个更普遍的问题是,即使在您的客户端中注释掉
System.exit(..)
,这种情况下的异常处理只会在控制台上打印一些内容,但会抑制发生的异常,因此客户端的用户 class 没有简单的方法来发现发生了什么不好的事情并处理这种情况。由于某种原因无法建立连接,她将只剩下一个不工作的客户端。您仍然可以调用sendMessage(..)
,但随后会发生错误。 - 还有更多的问题我就不在这里说了,因为太详细了。
所以您想重构您的代码,使其更易于维护,也更易于测试。这是测试驱动开发真正有用的地方。它是一种设计工具,主要不是质量管理工具。
这个怎么样?我仍然对此不满意,但它显示了如何更容易地测试代码:
回显服务器:
现在的服务器
- 负责在单独的线程中侦听服务器端口 4444。不再需要在测试中启动额外的线程
- 为每个传入连接生成另一个新线程
- 可以同时处理多个连接(另见下面相应的集成测试)
- 有一个
close()
方法并实现了AutoCloseable
,即可以手动关闭或通过 try-with-resources 关闭。
我还添加了一些日志记录,主要用于演示目的,因为如果测试通过,通常不会记录任何内容。
package de.scrum_master.Whosebug.q55475971;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer implements AutoCloseable {
private ServerSocket serverSocket;
public EchoServer(int portNumber) throws IOException {
this(new ServerSocket(portNumber));
}
public EchoServer(ServerSocket serverSocket) throws IOException {
this.serverSocket = serverSocket;
listen();
System.out.printf("%-25s - Echo server started%n", Thread.currentThread());
}
private void listen() {
Runnable listenLoop = () -> {
System.out.printf("%-25s - Starting echo server listening loop%n", Thread.currentThread());
while (true) {
try {
echo(serverSocket.accept());
} catch (IOException e) {
System.out.printf("%-25s - Stopping echo server listening loop%n", Thread.currentThread());
break;
}
}
};
new Thread(listenLoop).start();
}
private void echo(Socket clientSocket) {
Runnable echoLoop = () -> {
System.out.printf("%-25s - Starting echo server echoing loop%n", Thread.currentThread());
try (
Socket socket = clientSocket;
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println(inputLine);
System.out.printf("%-25s - Echoing back message: %s%n", Thread.currentThread(), inputLine);
}
System.out.printf("%-25s - Stopping echo server echoing loop%n", Thread.currentThread());
} catch (IOException e) {
e.printStackTrace();
}
};
new Thread(echoLoop).start();
}
@Override
public void close() throws Exception {
System.out.printf("%-25s - Shutting down echo server%n", Thread.currentThread());
if (serverSocket != null) serverSocket.close();
}
}
回声客户端:
现在的客户
- 不再吞下异常,而是让它们发生并由用户处理
- 可以通过其构造函数之一注入一个
Socket
实例,这允许轻松模拟并使 class 更可测试 - 有一个
close()
方法并实现了AutoCloseable
,即可以手动关闭或通过 try-with-resources 关闭。
我还添加了一些日志记录,主要用于演示目的,因为如果测试通过,通常不会记录任何内容。
package de.scrum_master.Whosebug.q55475971;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class EchoClient implements AutoCloseable {
private Socket echoSocket;
private PrintWriter out;
private BufferedReader in;
public EchoClient(String hostName, int portNumber) throws IOException {
this(new Socket(hostName, portNumber));
}
public EchoClient(Socket echoSocket) throws IOException {
this.echoSocket = echoSocket;
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));
System.out.printf("%-25s - Echo client started%n", Thread.currentThread());
}
public String sendMessage(String msg) throws IOException {
System.out.printf("%-25s - Sending message: %s%n", Thread.currentThread(), msg);
out.println(msg);
return in.readLine();
}
@Override
public void close() throws Exception {
System.out.printf("%-25s - Shutting down echo client%n", Thread.currentThread());
if (out != null) out.close();
if (in != null) in.close();
if (echoSocket != null) echoSocket.close();
}
}
集成测试:
这类似于您自己和 masooh 的解决方案,但使用更新的客户端和服务器 classes。您会看到现在客户端和服务器的可测试性是多么容易。实际上测试的目的是只测试客户端,使用服务器只是因为它是一个集成测试。但是因为 classes 的代码结构现在更加线性,IT 实际上为客户端和服务器创建了 100% 的行覆盖率。
package de.scrum_master.Whosebug.q55475971
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientIT extends Specification {
static final int SERVER_PORT = 4444
@Shared
EchoClient echoClient
@Shared
EchoServer echoServer
void setupSpec() {
echoServer = new EchoServer(SERVER_PORT)
echoClient = new EchoClient("localhost", SERVER_PORT)
}
void cleanupSpec() {
echoClient?.close()
echoServer?.close()
}
@Unroll
def "server echoes client message '#message'"() {
expect:
echoClient.sendMessage(message) == message.toString()
where:
message << ["echo", "Hello world!", null]
}
def "multiple echo clients"() {
given:
def echoClients = [
new EchoClient("localhost", SERVER_PORT),
new EchoClient("localhost", SERVER_PORT),
new EchoClient("localhost", SERVER_PORT)
]
expect:
echoClients.each {
assert it.sendMessage("foo") == "foo"
}
echoClients.each {
assert it.sendMessage("bar") == "bar"
}
cleanup:
echoClients.each { it.close() }
}
@Unroll
def "client creation fails with #exceptionType.simpleName when using illegal #connectionInfo"() {
when:
new EchoClient(hostName, portNumber)
then:
thrown exceptionType
where:
connectionInfo | hostName | portNumber | exceptionType
"host name" | "does.not.exist" | SERVER_PORT | UnknownHostException
"port number" | "localhost" | SERVER_PORT + 1 | IOException
}
}
单元测试:
我把这个留到最后,因为你原来的问题是关于嘲笑的。所以现在我将向您展示如何通过构造函数创建模拟套接字(或更准确地说是存根)并将其注入到您的客户端中。 IE。单元测试不打开任何真正的端口或套接字,它甚至不使用服务器 class。它实际上只对客户端进行单元测试 class。连抛出的异常都经过测试
顺便说一句,stub 有点复杂,实际上表现得像 echo 服务器。我通过管道流做到了这一点。当然,也可以创建一个更简单的 mock/stub,它只是 returns 固定结果。
package de.scrum_master.Whosebug.q55475971
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientTest extends Specification {
@Unroll
def "server echoes client message '#message'"() {
given:
def outputStream = new PipedOutputStream()
def inputStream = new PipedInputStream(outputStream)
def echoClient = new EchoClient(
Stub(Socket) {
getOutputStream() >> outputStream
getInputStream() >> inputStream
}
)
expect:
echoClient.sendMessage(message) == message.toString()
cleanup:
echoClient.close()
where:
message << ["echo", "Hello world!", null]
}
def "client creation fails for unreadable socket streams"() {
when:
new EchoClient(
Stub(Socket) {
getOutputStream() >> { throw new IOException("cannot read output stream") }
getInputStream() >> { throw new IOException("cannot read input stream") }
}
)
then:
thrown IOException
}
def "client creation fails for unknown host name"() {
when:
new EchoClient("does.not.exist", 4444)
then:
thrown IOException
}
}
P.S.: 你可以为服务器编写一个类似的单元测试,而不是使用客户端 class 或真正的套接字,但我已经准备好让你自己想办法了服务器 class 也通过构造函数注入接受套接字。但是您会注意到,使用模拟测试服务器的 echo()
方法并不那么容易,所以也许您想在那里进行更多重构。