我们有一个(Spring) REST,我的任务是为它编写集成测试。我编写了各种测试REST的方法,然后决定创建一个集成测试类,它处理所有可以在PUT请求中针对REST提交的JSON文件,并测试安全性以及各种获取和删除。这个测试类目前发出586个HTTP请求(GET、PUT和DELETE),通常在30秒左右(大约20个HTTP请求/秒)。在对Spring、Apache组件和其他lib感到沮丧之后,我决定在没有任何第三方库/框架的情况下编写我的集成测试。起初,我把我的测试分成几个类,然后决定把所有的内容都放到一个类中。我通常编写较小的类,但希望在一个更大的类中进行实验,以了解这个类的外观/感觉。此代码不使用第三方库,只需要JSON文件和正在运行的REST来处理。
你对我是怎么安排这件事的?如果你是我的同事,你会鄙视被指派去修改这段代码吗?如果你认为这门课应该“分开”,那会有什么好处呢?
我是在IntelliJ IDEA中开发的,它目前没有报告该类的警告/错误。
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
// These tests expect the enable assertions flag ("-ea") to be set as a VM option
public final class RestApiIntTests {
private static final String BASIC_AUTH_TOKEN = Base64.getEncoder().encodeToString("sa:sa".getBytes(StandardCharsets.UTF_8));
private static final Map<String,String> STANDARD_HEADERS = new HashMap<String,String>() {{
put("Authorization", "Basic " + BASIC_AUTH_TOKEN);
put("Content-Type", "application/json");
put("Accept", "application/json");
}};
private enum HttpVerb {
DELETE, GET, PUT
}
public static void main(final String[] args) throws Exception {
final long startTime = new Date().getTime();
// Maps json filenames to corresponding REST endpoint
final Map<String,String> jsonFilenameToEndpointMapper = new TreeMap<String,String>() {{
put("alert","/alerts");
put("chart","/charts");
put("clientCustom","/clientCustom");
put("connection","/connections");
put("group","/groups");
put("report","/reports");
put("role","/roles");
put("service","/services");
put("user","/users");
}};
// ***********************************
// Test GET requests at root endpoints
// ***********************************
for (final Map.Entry<String, String> entry : jsonFilenameToEndpointMapper.entrySet()) {
final String curEndpoint = entry.getValue();
System.out.println("get " + curEndpoint);
final String getResp = restCall(HttpVerb.GET, "", STANDARD_HEADERS, curEndpoint);
assert getResp.split("200 OK", -1).length-1 > 0 : getResp;
}
// **********
// JSON TESTS
// **********
try (final DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(""))) {
final TreeSet<String> fileNames = new TreeSet<>();
// First, gather file names for processing
for (final Path file: stream) {
final String fileName = file.getFileName().toString();
// Specify whatever regex you want for matching test json files...
if (fileName.matches(".*.json")) {
fileNames.add(fileName);
}
}
// Now, process files in order
for (final String fileName : fileNames) {
final String curJsonFileName = fileName.substring(0, fileName.indexOf("."));
final String curEndpoint = getCurrentEndpoint(curJsonFileName, jsonFilenameToEndpointMapper);
final String curNameEndpoint = curEndpoint + "/restApiIntTest"; // "restApiIntTest" should be the "name" field value in the json files
if (curJsonFileName.equals("clientCustom")) {
final String fieldToken = "\"applicationName\":";
// ********************
// Update Client Custom
// ********************
System.out.println("update " + curJsonFileName);
final String[] respLines = restCall(HttpVerb.GET, "", STANDARD_HEADERS, curEndpoint).split("\r\n");
final String clientCustomJson = extractJsonFromResponse(respLines, fieldToken);
final int startIndex = clientCustomJson.indexOf(fieldToken) + fieldToken.length();
final int endIndex = clientCustomJson.indexOf(",\"", startIndex);
final String origAppName = clientCustomJson.substring(startIndex, endIndex);
final String newAppName = "\"IR360-DEV-" + new Date().getTime() + "\"";
final String newClientCustomJson = clientCustomJson.replace(fieldToken + origAppName, fieldToken + newAppName);
final String putResp = restCall(HttpVerb.PUT, newClientCustomJson, STANDARD_HEADERS, curEndpoint);
assert putResp.toLowerCase().contains("success") : putResp;
final String[] newRespLines = restCall(HttpVerb.GET, "", STANDARD_HEADERS, curEndpoint).split("\r\n");
final String newClientCustomResp = extractJsonFromResponse(newRespLines, fieldToken);
assert newClientCustomResp.contains(newAppName) : newClientCustomResp;
} else {
// ******
// Create
// ******
System.out.println("create " + curJsonFileName);
final InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(curJsonFileName + ".json");
final String curFileJson = new Scanner(inputStream, StandardCharsets.UTF_8.name()).useDelimiter("\\A").next();
// Make sure record doesn't exist before creating...
restCall(HttpVerb.DELETE, "", STANDARD_HEADERS, curNameEndpoint);
final String createResp = restCall(HttpVerb.PUT, curFileJson, STANDARD_HEADERS, curEndpoint);
assert createResp.toLowerCase().contains("success") : createResp;
// ***
// Get
// ***
System.out.println("get " + curJsonFileName);
final String getResp = restCall(HttpVerb.GET, "", STANDARD_HEADERS, curNameEndpoint);
assert getResp.split("\"guid\"", -1).length-1 > 0 : getResp;
// ******
// Update
// ******
System.out.println("update " + curJsonFileName);
// Expects original json fields/values setup like this...
// "<curUpdateField>":""
// "modifiedBy":""
// "modifiedDate":null
final String updatedDesc = "\"RestApiIntTests updated description...\"";
final String updateField = curJsonFileName.equals("user") ? "\"comment\":" : "\"description\":";
final String updatedJson = extractJsonFromResponse(getResp.split("\r\n"), "\"guid\":")
.replace(updateField + "\"\"", updateField + updatedDesc)
.replace("\"modifiedBy\":\"\"", "\"modifiedBy\":\"test\"")
.replace("\"modifiedDate\":null", "\"modifiedDate\":\"" + new Date().getTime() + "\"");
final String updResp = restCall(HttpVerb.PUT, updatedJson, STANDARD_HEADERS, curEndpoint);
assert ( ! updResp.toLowerCase().contains("error")) : updResp;
final String updGetResp = restCall(HttpVerb.GET, "", STANDARD_HEADERS, curNameEndpoint);
assert updGetResp.contains(updatedDesc) : updGetResp;
// ******
// Delete
// ******
System.out.println("delete " + curJsonFileName);
final String delResp = restCall(HttpVerb.DELETE, "", STANDARD_HEADERS, curNameEndpoint);
assert delResp.toLowerCase().contains("success") : delResp;
}
}
} catch (IOException | DirectoryIteratorException x) {
// IOException can never be thrown by the iteration.
// In this snippet, it can only be thrown by newDirectoryStream.
System.err.println(x);
}
// **************
// SECURITY TESTS
// **************
// ***************
// Basic Auth Test
// ***************
System.out.println("basicAuthTest");
final String basicAuthResp = restCall(HttpVerb.GET, "", new HashMap<String, String>() {{
put("Authorization", "Basic " + BASIC_AUTH_TOKEN);
}}, "/");
assert basicAuthResp.toLowerCase().contains("hello") : basicAuthResp;
// *********************
// Remember Me Auth Test
// *********************
System.out.println("rememberMeAuthTest");
// Do initial request with basic auth and get the remember-me cookie
final String rememberMeNoExp = getRememberMeCookie(null);
assert (rememberMeNoExp != null) && (rememberMeNoExp.length() > 0);
// Now make a request with only the remember-me cookie present
final String rememberMeResp = restCall(HttpVerb.GET, "", new HashMap<String, String>() {{
put("remember-me", rememberMeNoExp);
}}, "/");
assert rememberMeResp.toLowerCase().contains("hello") : rememberMeResp;
// ************************************
// Remember Me Auth Cookie Expired Test
// ************************************
System.out.println("rememberMeAuth_CookieExpiredTest");
// Do initial request with basic auth and get the remember-me cookie
// (set the "tokenValiditySeconds" header to a low value to expire it right away)
final String rememberMeExp = getRememberMeCookie(1);
assert (rememberMeExp != null) && (rememberMeExp.length() > 0);
// Now make a request with only the remember-me cookie present (should throw Exception as it will be expired when called)
Thread.sleep(2000);
final String rememberMeExpResp = restCall(HttpVerb.GET, "", new HashMap<String, String>() {{
put("remember-me", rememberMeExp);
}}, "/");
assert rememberMeExpResp.toLowerCase().contains("unauthorized") : rememberMeExpResp;
// **************
// X509 Auth Test
// **************
System.out.println("x509AuthTest");
/*
* This is an X.509 certificate encoded as a Base64 String
* If this String doesn't work, you can replace it using the following steps
* 1. Generate a keypair using the JDK's "keytool" utility
* For example, the following creates a key with a common name of "sa" (the common name is extracted from the cert and used as the username to login to IR360),
* an alias of "x509" and a validity of 3650 days (about 10 years)
*
* keytool -genkeypair -dname "cn=sa, ou=dev, o=avada, c=US" -alias x509 -validity 3650
*
* 2. Export the cert onto your machine (the following will create "x509test.cer" in your current directory)
*
* keytool -exportcert -alias x509 -file x509test.cer
*
* 3. Base64 encode the contents of the exported cert and replace the String below with that value
* For example, open the file using Notepad++, select all and then choose Plugins -> MIME Tools -> Base64 Encode
*/
final String x509Resp = restCall(HttpVerb.GET, "", new HashMap<String, String>() {{
put("X.509", "MIICzTCCAougAwIBAgIEKdxA4zALBgcqhkjOOAQDBQAwODELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBWF2YWRhMQwwCgYDVQQLEwNkZXYxCzAJBgNVBAMTAnNhMB4XDTE2MDUwNTIxMDQwMVoXDTI2MDUwMzIxMDQwMVowODELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBWF2YWRhMQwwCgYDVQQLEwNkZXYxCzAJBgNVBAMTAnNhMIIBuDCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BYHPUCgYEA9+GghdabPd7LvKtcNrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6Ae1UlZAFMO/7PSSoDgYUAAoGBALkISGRy+xusKkizEYMft0f3f3y+UONIWmSQ8ZqkxGT5WIr7msdyEdbxMBrL3ej1gZIjXDI6oLJfgH+M7lDMuKlsR4h96iXAQFIrZFWqjBvh/wFmgbsBzisTEE4NFsfXSAc6WJK6jnq3+6HRr4wH/lvN76oDPsFRBwZ48GUNi+iXoyEwHzAdBgNVHQ4EFgQUrg29qmlxC9T18Rmn/JUcD1RhxV4wCwYHKoZIzjgEAwUAAy8AMCwCFENOqc2FsDAc+uQz+/fafBVsf/RiAhQRAjEKobdD9eNbZCIiNgRYAbeDQQ==");
}}, "/");
assert x509Resp.toLowerCase().contains("hello") : x509Resp;
// *********************
// Output total run time
// *********************
final long totalSeconds = (new Date().getTime() - startTime) / 1000;
System.out.println("Total run time seconds=" + totalSeconds);
}
private static String getCurrentEndpoint(final String curJsonFileName, final Map<String,String> jsonFilenameToEndpointMapper) {
for (final Map.Entry<String, String> entry : jsonFilenameToEndpointMapper.entrySet()) {
if (curJsonFileName.startsWith(entry.getKey())) {
return entry.getValue();
}
}
return "";
}
private static String extractJsonFromResponse(final String[] responseLines, final String token) {
for (final String s : responseLines) {
if (s.contains(token)) {
return s.substring(0, s.lastIndexOf("}") + 1);
}
}
return "";
}
private static String getRememberMeCookie(final Integer tokenValiditySeconds) throws Exception {
final String uri = (tokenValiditySeconds == null) ? "/?remember-me=true" : "/?remember-me=true&tokenValiditySeconds=" + tokenValiditySeconds;
final String[] responseLines = restCall(HttpVerb.GET, "", new HashMap<String, String>() {{ put("Authorization", "Basic " + BASIC_AUTH_TOKEN); }}, uri).split("\r\n");
final String startToken = "remember-me=";
for (final String line : responseLines) {
if (line.contains(startToken)) {
final int start = line.indexOf(startToken) + startToken.length();
final int end = line.indexOf("; ", start);
return line.substring(start, end);
}
}
return null;
}
private static final InetAddress INET_ADDRESS = getInetAddress();
private static InetAddress getInetAddress() {
try {
return InetAddress.getByName("localhost");
} catch (UnknownHostException e) {
System.err.println(e.getMessage());
System.exit(1);
}
return null;
}
private static String restCall(final HttpVerb httpVerb, final String reqBody, final Map<String,String> headers, final String path) {
try (final Socket socket = new Socket(INET_ADDRESS, 8080);
final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
final BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())))
{
socket.setSoTimeout(30000); // milliseconds
// Make HTTP request
// Not sure why, but content somehow has \u0000 in it...need to strip those out...
final String content = reqBody.replace("\u0000", "");
bw.write(httpVerb.toString() + " /IR360/rest" + path + " HTTP/1.0\r\n");
for (Map.Entry<String,String> entry : headers.entrySet()) {
bw.write(entry.getKey() + ": " + entry.getValue() + "\r\n");
}
if (content.length() > 0) {
bw.write("Content-Length: " + content.length() + "\r\n");
}
bw.write("\r\n");
if (content.length() > 0) {
bw.write(content);
}
bw.flush();
// Process HTTP response
final StringBuilder sb = new StringBuilder();
int i;
while( (i = br.read()) != -1){
sb.append((char)i);
}
return sb.toString();
} catch (Exception e) {
System.err.println("Problem communicating with REST API : " + e);
System.exit(1);
}
return "--";
}
}发布于 2016-11-20 00:45:45
如果你认为这门课应该“分开”,那会有什么好处呢?
人们可以在一个可接受的时间内阅读这段代码,如果需要的话,甚至可以修改它。
您正在询问将代码拆分为多个类的问题,但我认为您应该后退一步,专注于将代码拆分成多个方法。您的代码几乎是完成所有任务的一种大型方法。看起来你觉得它需要分割--你用注释来标记部分。
如果我只想理解你的全部代码,我为什么要读它呢?使用现有代码的人最感兴趣的是理解代码是干什么的,而不是它是如何工作的。你需要抽象化东西。类、方法是帮助实现这一目标的工具。
发布于 2016-11-21 18:51:28
好的,所以我假设这是根据你的风格指南格式化的。
接下来,使用单元测试库而不是assert,实际上assert应该用于在常规程序中断言“不可能发生”的事情,这就是为什么它甚至可以被禁用用于生产。
可能有一个选项可以在与实际端点相同的JVM中运行,以防止实际连接--如果用于HTTP调用的时间太长,或者考虑同时运行测试。
所有的finals都是强制性的吗?否则我就放弃它,它对可读性没有太大帮助。
x509Resp的初始化非常可怕,把证书放到一个单独的资源文件中。
在错误情况下返回可行的值是不好的,因为getCurrentEndpoint和extractJsonFromResponse考虑在没有找到值的情况下抛出异常,或者返回null或其他什么,但是要确保空字符串不会被使用--这也会更容易调试。
捕捉一个只调用System.exit的异常似乎毫无意义,只要让它传播,异常就会被打印出来。
为什么HTTP请求是手动创建的?看看其中一个库,并使用它们。例如,对于各种web服务来说,泽西岛是很好的,但是如果这是一个太大的改变,也许仅仅Apache HttpClient也是可以的。如果正确地解析了JSON并使用了库,那么所有的字符串修改都应该消失。
为什么里面有一个Thread.sleep?这句话并没有解释,所以我认为这是不必要的。
所以,不是所有的错误,只需使用库并将逻辑移到方法中--至少,这么长的函数(main)有点不可读,例如,大的if/else块实际上也可以移到单独的方法中。
https://codereview.stackexchange.com/questions/147468
复制相似问题