首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >REST集成测试

REST集成测试
EN

Code Review用户
提问于 2016-11-18 21:17:45
回答 2查看 267关注 0票数 4

我们有一个(Spring) REST,我的任务是为它编写集成测试。我编写了各种测试REST的方法,然后决定创建一个集成测试类,它处理所有可以在PUT请求中针对REST提交的JSON文件,并测试安全性以及各种获取和删除。这个测试类目前发出586个HTTP请求(GET、PUT和DELETE),通常在30秒左右(大约20个HTTP请求/秒)。在对Spring、Apache组件和其他lib感到沮丧之后,我决定在没有任何第三方库/框架的情况下编写我的集成测试。起初,我把我的测试分成几个类,然后决定把所有的内容都放到一个类中。我通常编写较小的类,但希望在一个更大的类中进行实验,以了解这个类的外观/感觉。此代码不使用第三方库,只需要JSON文件和正在运行的REST来处理。

你对我是怎么安排这件事的?如果你是我的同事,你会鄙视被指派去修改这段代码吗?如果你认为这门课应该“分开”,那会有什么好处呢?

我是在IntelliJ IDEA中开发的,它目前没有报告该类的警告/错误。

代码语言:javascript
复制
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 "--";
    }
}
EN

回答 2

Code Review用户

发布于 2016-11-20 00:45:45

如果你认为这门课应该“分开”,那会有什么好处呢?

人们可以在一个可接受的时间内阅读这段代码,如果需要的话,甚至可以修改它。

您正在询问将代码拆分为多个类的问题,但我认为您应该后退一步,专注于将代码拆分成多个方法。您的代码几乎是完成所有任务的一种大型方法。看起来你觉得它需要分割--你用注释来标记部分。

如果我只想理解你的全部代码,我为什么要读它呢?使用现有代码的人最感兴趣的是理解代码是干什么的,而不是它是如何工作的。你需要抽象化东西。类、方法是帮助实现这一目标的工具。

票数 2
EN

Code Review用户

发布于 2016-11-21 18:51:28

好的,所以我假设这是根据你的风格指南格式化的。

接下来,使用单元测试库而不是assert,实际上assert应该用于在常规程序中断言“不可能发生”的事情,这就是为什么它甚至可以被禁用用于生产。

可能有一个选项可以在与实际端点相同的JVM中运行,以防止实际连接--如果用于HTTP调用的时间太长,或者考虑同时运行测试。

所有的finals都是强制性的吗?否则我就放弃它,它对可读性没有太大帮助。

x509Resp的初始化非常可怕,把证书放到一个单独的资源文件中。

在错误情况下返回可行的值是不好的,因为getCurrentEndpointextractJsonFromResponse考虑在没有找到值的情况下抛出异常,或者返回null或其他什么,但是要确保空字符串不会被使用--这也会更容易调试。

捕捉一个只调用System.exit的异常似乎毫无意义,只要让它传播,异常就会被打印出来。

为什么HTTP请求是手动创建的?看看其中一个库,并使用它们。例如,对于各种web服务来说,泽西岛是很好的,但是如果这是一个太大的改变,也许仅仅Apache HttpClient也是可以的。如果正确地解析了JSON并使用了库,那么所有的字符串修改都应该消失。

为什么里面有一个Thread.sleep?这句话并没有解释,所以我认为这是不必要的。

所以,不是所有的错误,只需使用库并将逻辑移到方法中--至少,这么长的函数(main)有点不可读,例如,大的if/else块实际上也可以移到单独的方法中。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/147468

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档