Học API Automation Testing Với Rest Assured

Học API Automation Testing Với Rest Assured

Rest Assured là một trong những thư viện được dùng nhiều nhất trong API Automation testing. Trong tutorial này mình sẽ hướng dẫn các bạn về các khái niệm như là Rest API, API Testing, API Automation Testing,...

API là gì?

API là viết tắt của (Application Programming Interface) – Giao diện lập trình ứng dụng, nó là một tập hợp các functions mà có thể được truy cập bởi các ứng dụng khác. Do đó, nó như là một giao diện giữa các hệ thống phần mềm khác nhau và thiết lập sự tương tác và trao đổi dữ liệu của chúng.

API Testing là gì?

Trong nền công nghiệp phát triển phần mềm hiện đại, rất nhiều các ứng dụng / hệ thống được thiết kế dựa trên mô hình 3 lớp (3-tier architecture model).

Ba lớp đó là:

  • Presentation Tier – Tầng tương tác trực tiếp với người dùng, dùng để hiển thị các thành phần giao diện để tương tác với người dùng như tiếp nhận thông tin, thông báo lỗi
  • Logic Tier – Tầng logic, tầng này cũng có thể được gọi là business tier, nhiệm vụ của tầng này là đảm nhận và xử lí các business của hệ thống. Tầng này cũng làm nhiệm vụ di chuyển, xử lí thông tin giữa 2 tầng Presentation và Data Tier
  • Data Tier - nơi lưu trữ và trích xuất dữ liệu từ các hệ quản trị CSDL hay các file trong hệ thống. Cho phép tầng Business logic thực hiện các truy vấn dữ liệu.

3 layers kể trên sẽ giao tiếp với nhau thông qua các dịch vụ (services) mà mỗi layer cung cấp để tạo nên ứng dụng, lớp này cũng không cần biết bên trong lớp kia làm gì mà chỉ cần biết lớp kia cung cấp dịch vụ gì cho mình và sử dụng nó mà thôi.

Lớp Logic bao gồm tất cả các bussiness logic của hệ thống, nó phức tạp hơn tất cả các lớp còn lại, do đó việc thực hiện kiểm thử trên lớp này là rất cần thiết. Việc kiểm thử trên lớp bussiness logic này được gọi là API Testing.

Đối với UI Testing, chúng ta test dựa trên các hành vi của người dùng như nhập dữ liệu qua bàn phím, thực hiện click một button nào đó trên screen,… thì đối với API Testing, chúng ta thực hiện gửi các request và check xem output từ API trả về cho chúng ta xem hệ thống trả về có đúng như chúng ta mong muốn hay không. Dữ liệu mà API trả về cho chúng ta có thể là JSON với RESTful API hoặc là XML với SOAP.

Để hiểu thêm về REST và SOAP thì các bạn có thể tham khảo bài viết Chọn web service tốt nhất

REST Assured là gì

Rest-assured (RestA) là 1 thư viện Java DSL được xây dựng trên nền HTTP Builder (thư viện tạo HTTP request), cho phép thực hiện gửi request và kiểm tra response. Nói đơn giản là một thư viện được xây dựng sẵn để việc thực hiện kiểm thử API trở nên dễ dàng hơn. Thực tế, có rất nhiều thư viện mà chúng ta có thể dùng, nhưng kinh nghiệm cho thấy, Rest Assured dễ dùng hơn hết, nhiều chức năng hơn, và đặc biệt là nó có một cộng đồng hỗ trợ rất lớn.

REST API testing với REST Assured

Lý thuyết nhiều rồi, chúng ta sẽ thực hiện thực hiện api testing qua ví dụ thực tế sau :D

Dự án A được đảm nhận xây dựng một hệ thống tìm kiếm video cho khách hàng JP, ngoài việc xây dựng 1 website tìm kiếm thì khách hàng còn mong muốn public API của hệ thống ra ngoài. Vậy là tester trong dự án lại phải đảm nhận vai trò test các api endpoint mà hệ thống sẽ public.

Đề bài đặt ra cho tester như sau:

  • Test case 1: Search video với từ khoá cho trước, giới hạn số video trả về là 4 videos.
    • Từ khoá: API Testing
    • Parameter:
      • {tukhoa}: Từ khoá của video mong muốn search
      • {soluongvideo}: Số lượng video mong muốn trả về, trong test case mong muốn là 4
    • URL Endpoint: http://api.5min.com/search/{tukhoa}/videos.json?num_of_videos={soluongvideo}
    • Kết quả mong muốn: Trả về kết quả dưới dạng JSON, trong đó có link dẫn đến các videos, tiêu đề và mô tả của các video đó
    • Điều kiện để test case pass:
      • Trong kết quả trả về phải có HTTP Status Code
      • Kết quả trả về phải chứa từ khoá tìm kiếm
      • Chỉ có tối đa 4 videos được trả về
      • Các video trả về không trùng lặp nhau
  • Test case 2: Search video với id của video, nhưng lần này sẽ trả về thông tin chi tiết của video đó và các video liên quan đến video đó (kiểu như suggestion video ở youtube vậy), cũng giới hạn số lượng các video liên quan trả về là 4
    • Parameter:
      • { video_id }: Từ khoá của video mong muốn search
      • {soluongvideo}: Số lượng video mong muốn trả về, trong test case mong muốn là 4
    • URL Endpoint: http://api.5min.com/video/list/info.json?video_ids={video_id}&num_related_return={soluongvideo}
    • Kết quả mong muốn: Trả về kết quả dưới dạng JSON, trong đó có thông tin chi tiết về video đó và các video liên quan
    • Điều kiện để test case pass:
      • Trong kết quả trả về phải có HTTP Status Code
      • Kết quả trả về phải liên quan tới video được tìm kiếm
      • Chỉ có tối đa 4 videos được trả về
      • Các video liên quan được trả về không trùng lặp nhau

Vậy, giải pháp của anh tester là gì? Hãy cùng mình sang phần tiếp theo nhé

REST API testing với REST Assured – (cont.)

Để giải quyết bài toán đưa ra ở trên, tester phải xây dựng 1 framework và tích hợp Rest Assured làm thư viện để testing API. Framework tester xây dựng cũng phải đáp ứng tiêu chí Re-usability để có thể test cho các api endpoint khác mà không phải mất công viết lại code. 

Bây giờ chúng ta bắt đầu !!

1. Xây dựng Project structure.

Đầu tiên, chúng ta sẽ khởi tạo 1 Maven project, và setup các thư mục như bên dưới:

  • Utils folder sẽ chứa 2 class là HelperMethods và RestUtil
    • HelperMethods: Class này sẽ gồm các helpers có thể reusalbe
    • RestUtil: class này sẽ bao gồm các methods liên quan đến thư viện Rest Assured 
  • Trong thư mục ApiTests sẽ có 2 class là Example1Test và Example2Test, đây là 2 classes chứa các test cases và các assertions
  • Trong thư mục TestSuite sẽ có class AllApiTest, đây là class Test Runner của chúng ta, class này sẽ đảm nhận việc run tất cả các test cases

2. Cài đặt các dependencies

  • Chúng ta sẽ add cái dependencies sau vào pom.xml.
    • Junit Library: Cái này là test framework
    • Hamcrest library: thư viện dành cho các method assertion
    • Jayway Rest Assured: Đây là Rest assured, thư viện mà chúng ta nhắc đến nãy giờ trong bài viết :D
  • Cấu trúc file pom.xml của chúng ta sẽ như bên dưới:
    <?xml version="1.0"encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.5min.apitest</groupId>
        <artifactId>5min-apitest</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <!--Dependencies-->
    
        <dependencies>
            <dependency>
                <groupId>org.hamcrest</groupId>
                <artifactId>hamcrest-all</artifactId>
                <version>1.3</version>
            </dependency>
            <dependency>
                <groupId>org.hamcrest</groupId>
                <artifactId>hamcrest-junit</artifactId>
                <version>2.0.0.0</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
            </dependency>
            <dependency>
                <groupId>com.jayway.restassured</groupId>
                <artifactId>json-schema-validator</artifactId>
                <version>2.8.0</version>
            </dependency>
            <dependency>
                <groupId>com.jayway.restassured</groupId>
                <artifactId>rest-assured</artifactId>
                <version>2.8.0</version>
            </dependency>
        </dependencies>
    
        <!--Profiles-->
    
        <profiles>
            <profile>
                <id>AllApiTests</id>
                <activation>
                    <activeByDefault>true</activeByDefault>
                </activation>
                <build>
                    <pluginManagement>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-surefire-plugin</artifactId>
                                <version>2.19.1</version>
                                <configuration>
                                    <includes>
                                        <include>**/AllApiTest.class</include>
                                    </includes>
                                </configuration>
                            </plugin>
                        </plugins>
                    </pluginManagement>
                </build>
            </profile>
        </profiles>
    </project>

3. Sau khi add xong các dependencies thì chúng ta sẽ bắt tay vào code

  • RestUtil.java

    Đây là class giúp chúng ta viết các common methods để giúp chúng ta tiện sử dụng sau này

    package Utils;
    
    import com.jayway.restassured.RestAssured;
    import com.jayway.restassured.http.ContentType;
    import com.jayway.restassured.path.json.JsonPath;
    import com.jayway.restassured.response.Response;
    import static com.jayway.restassured.RestAssured.*;
    
    public class RestUtil {
    
        //Global Setup Variables
    
        public static String path; //Rest request path
    
        /*
        ***Sets Base URI***
        Before starting the test, we should set the RestAssured.baseURI
        */
    
        public static void setBaseURI (String baseURI){
            RestAssured.baseURI = baseURI;
        }
    
        /*
        ***Sets base path***
        Before starting the test, we should set the RestAssured.basePath
        */
    
        public static void setBasePath(String basePathTerm){
            RestAssured.basePath = basePathTerm;
        }
    
        /*
        ***Reset Base URI (after test)***
        After the test, we should reset the RestAssured.baseURI
        */
    
        public static void resetBaseURI (){
            RestAssured.baseURI = null;
        }
    
        /*
        ***Reset base path (after test)***
        After the test, we should reset the RestAssured.basePath
        */
    
        public static void resetBasePath(){
            RestAssured.basePath = null;
        } 
    
        /*
        ***Sets ContentType***
        We should set content type as JSON or XML before starting the test
        */
    
        public static void setContentType (ContentType Type){
            given().contentType(Type);
        }
    
        /*
        ***search query path of first example***
        It is  equal to "barack obama/videos.json?num_of_videos=4"
        */
    
        public static void  createSearchQueryPath(String searchTerm, String jsonPathTerm, String param, String paramValue) {
            path = searchTerm + "/" + jsonPathTerm + "?" + param + "=" + paramValue;
        }
    
        /*
        ***Returns response***
        We send "path" as a parameter to the Rest Assured'a "get" method
        and "get" method returns response of API
        */
    
        public static Response getResponse() {
    
            //System.out.print("path: " + path +"\n");
    
            return get(path);
        } 
    
        /*
        ***Returns JsonPath object***
        * First convert the API's response to String type with "asString()" method.
        * Then, send this String formatted json response to the JsonPath class and return the JsonPath
        */
    
        public static JsonPath getJsonPath (Response res) {
            String json = res.asString();
            //System.out.print("returned json: " + json +"\n");
    
            return new JsonPath(json);
        }
    }
  • HelperMethods.java

    Đây cũng là một class chứa các common functions để chúng ta có thể sử dụng lại trong 2 classes Example1Test và Example2Test

    package Utils;
    
    import com.jayway.restassured.path.json.JsonPath;
    import com.jayway.restassured.response.Response;
    import java.util.*;
    
    import static org.junit.Assert.assertEquals;
    
    public class HelperMethods {
    
        /*
        Verify the http response status returned. Check Status Code is 200?
        We can use Rest Assured library's response's getStatusCode method
        */
    
        public static void checkStatusIs200 (Response res) {
            assertEquals("Status Check Failed!", 200, res.getStatusCode());
        }
    
        /*
        Get Video Ids (For example 1)
        We can use get method of Rest Assured library's JsonPath Class's get method
        Part of a response is shown below:
        "items": [{
        "id": 519377522,
     ....
     We can get all id's with this code --> "jp.get("items.id");" this will return
     all id's under "items" tag.
        */
    
        public static ArrayList getVideoIdList (JsonPath jp) {
            ArrayList videoIdList = jp.get("items.id");
    
            return videoIdList;
        }
    
        /*
        Get Related Video Ids (For example 2)
        Structure of response is shown below:
        items:
     "related": [{
     "id": 519148754,
     ....
     In order to get all id's under related tag,
        We can use JsonPath's get method like "jp.get("items.related.id");"
        It will give us all id's under related tag.
        */
    
        public static ArrayList getRelatedVideoIdList (JsonPath jp) {
            //jp.get method returns all ids
            ArrayList relatedVideoList = jp.get("items.related.id");
    
            /*
            Result of relatedVideosList: [[519148754, 519115214, 519235328, 519235341]]
            I have to convert above result in this format: [519148754, 519115214, 519235328, 519235341]
            In order to split first element of "relatedVideosList" and assign it to a new ArrayList (as splittedRelatedVideoList)
            I did below operation.
            */
    
            ArrayList splittedRelatedVideoList = (ArrayList) relatedVideoList.get(0);
    
            return splittedRelatedVideoList;
        }
    
        //Merge videoIdList and relatedVideoIdList as mergedVideoList
        public  static ArrayList mergeLists (ArrayList videoList, ArrayList relatedVideoList){
            ArrayList mergedVideoList = new ArrayList(videoList);
            mergedVideoList.addAll(relatedVideoList);
    
            return mergedVideoList;
        }
    
        //Find Duplicate Videos
        public static boolean findDuplicateVideos (List<Integer> videoIdList) {
             for (int i=0; i< videoIdList.size(); i++) {
                if(Collections.frequency(videoIdList, videoIdList.get(i)) > 1){
                    System.out.println("This video id is duplicated: " + videoIdList.get(i));
    
                    return false;
                }
            }
    
            return true;
        }
    }​
  • Example1Test.java - Example2Test.java

Đây là 2 class quan trọng nhất, classes này chứa các assertions để kiểm thử các kết quả trả về từ api

  • Example1Test.java

    package ApiTests;
     
    import Utils.*;
    import com.jayway.restassured.http.ContentType;
    import com.jayway.restassured.path.json.JsonPath;
    import com.jayway.restassured.response.Response;
    import org.junit.*;
    import org.junit.runners.MethodSorters;
    import static org.junit.Assert.assertTrue;
     
     
    @FixMethodOrder(MethodSorters.NAME_ASCENDING) //For Ascending order test execution
    public class Example1Test {
     
        //First, I declared Response and JsonPath objects.
        private Response res = null; //Response object
        private JsonPath jp = null; //JsonPath object
     
        /*
        Second, we should do setup operations, get JSON response from the API and put it into JsonPath object
        Then we will do query and manipulations with JsonPath class’s methods.
        We can do all of the preparation operations after @Before Junit annotation.
        */
        @Before
        public void setup (){
            //Test Setup
            RestUtil.setBaseURI("http://api.5min.com"); //Setup Base URI
            RestUtil.setBasePath("search"); //Setup Base Path
            RestUtil.setContentType(ContentType.JSON); //Setup Content Type
            RestUtil.createSearchQueryPath("barack obama", "videos.json", "num_of_videos", "4"); //Construct the path
            res = RestUtil.getResponse(); //Get response
            jp = RestUtil.getJsonPath(res); //Get JsonPath
        }
     
        @Test
        public void T01_StatusCodeTest() {
            //Verify the http response status returned. Check Status Code is 200?
            HelperMethods.checkStatusIs200(res);
        }
     
        @Test
        public void T02_SearchTermTest() {
            //Verify the response contained the relevant search term (barack obama)
            Assert.assertEquals("Title is wrong!", ("Search results for \"barack obama\""), jp.get("api-info.title"));
            //assertThat(jp.get("api-info.title"), containsString("barrack obama"));
        }
     
        @Test
        public void T03_verifyOnlyFourVideosReturned() {
            //Verify that only 4 video entries were returned
            Assert.assertEquals("Video Size is not equal to 4", 4, HelperMethods.getVideoIdList(jp).size());
        }
     
        @Test
        public void T04_duplicateVideoVerification() {
            //Verify that there is no duplicate video
            assertTrue("Duplicate videos exist!", HelperMethods.findDuplicateVideos(HelperMethods.getVideoIdList(jp)));
        }
     
        @Test
        public void T05_printAttributes() {
            //Print video title, pubDate & duration
            printTitlePubDateDuration(jp);
        }
     
        @After
        public void afterTest (){
            //Reset Values
            RestUtil.resetBaseURI();
            RestUtil.resetBasePath();
        }
     
        //*******************
        //***Local Methods***
        //*******************
        //Prints Attributes
        private void printTitlePubDateDuration (JsonPath jp) {
            for(int i=0; i < HelperMethods.getVideoIdList(jp).size(); i++ ) {
                System.out.println("Title: " + jp.get("items.title[" + i + "]"));
                System.out.println("pubDate: " + jp.get("items.pubDate[" + i + "]"));
                System.out.println("duration: " + jp.get("items.duration[" + i + "]"));
                System.out.print("\n");
            }
        }
    }
    
  • Example2Test.java
    package ApiTests;
     
    import Utils.*;
    import com.jayway.restassured.http.ContentType;
    import com.jayway.restassured.path.json.JsonPath;
    import com.jayway.restassured.response.Response;
    import org.junit.*;
    import org.junit.runners.MethodSorters;
     
    import java.util.ArrayList;
     
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertTrue;
     
    @FixMethodOrder(MethodSorters.NAME_ASCENDING) //For Ascending order test execution
    public class Example2Test {
     
        private Response res = null; //Response
        private JsonPath jp = null; //JsonPath
     
        /*
        We should do setup operations, get JSON response from the API and put it into JsonPath object
        Then we will do query and manipulations with JsonPath class’s methods.
        We can do all of the preparation operations after @Before Junit annotation.
        */
        @Before
        public void setup (){
            //Test Setup
            RestUtil.setBaseURI("http://api.5min.com"); //Setup Base URI
            RestUtil.setBasePath("video"); //Setup Base Path
            //In this example, I assigned full path manually in below code line.
            RestUtil.path = "list/info.json?video_ids=519218045&num_related_return=4";
            RestUtil.setContentType(ContentType.JSON); //Setup Content Type
            res = RestUtil.getResponse(); //Get response
            jp = RestUtil.getJsonPath(res); //Set JsonPath
        }
     
        @Test
        public void T01_StatusCodeTest() {
            //Verify the http response status returned. Check Status Code is 200?
            HelperMethods.checkStatusIs200(res);
        }
     
        @Test
        public void T02_SearchTermTest() {
            //Verify the response contained the relevant search term (519218045)
            assertEquals("Id does not match!", "519218045", HelperMethods.getVideoIdList(jp).get(0).toString());
        }
     
        @Test
        public void T03_verifyExtraFourVideosReturned() {
            //Verify that extra 4 video entries were returned as related videos
            assertEquals("Related video Size is not equal to 4", 4, HelperMethods.getRelatedVideoIdList(jp).size());
        }
     
        @Test
        public void T04_duplicateVideoVerification() {
            //Check duplicate videos exist?
            assertTrue("Duplicate videos exist!", HelperMethods.findDuplicateVideos(getMergedVideoLists()));
        }
     
        @Test
        public void T05_printAttributes() {
            //Print attributes
            printAttributes(jp);
        }
     
        @After
        public void afterTest (){
            //Reset Values
            RestUtil.resetBaseURI();
            RestUtil.resetBasePath();
        }
     
        //*******************
        //***Local Methods***
        //*******************
        //Returns Merged Video Lists (Video List + Related Video List)
        private ArrayList getMergedVideoLists (){
            return HelperMethods.mergeLists(HelperMethods.getVideoIdList(jp), HelperMethods.getRelatedVideoIdList(jp));
        }
     
        //Prints Attributes
        private void printAttributes(JsonPath jp) {
            for(int i=0; i <getMergedVideoLists().size(); i++ ) {
                //Prints Video List Attributes
                if(jp.get("items.title[" + i + "]") != null) {
                    System.out.println("title: " + jp.get("items.title[" + i + "]"));
                    System.out.println("Tablets: " + jp.get("items.permittedDeviceTypes.Tablets[" + i + "]"));
                    System.out.println("Handsets: " + jp.get("items.permittedDeviceTypes.Handsets[" + i + "]"));
                    System.out.println("ConnectedDevices: " + jp.get("items.permittedDeviceTypes.ConnectedDevices[" + i + "]"));
                    System.out.println("Computers: " + jp.get("items.permittedDeviceTypes.Computers[" + i + "]"));
                    System.out.println("Duration: " + jp.get("items.duration[" + i + "]"));
                    System.out.print("\n");
     
                    //Check that sent video has related videos? If yes print their attributes
                    if (jp.get("items.related.title[" + i + "][" + i + "]") != null) {
                        for (int j = 0; j < HelperMethods.getRelatedVideoIdList(jp).size(); j++) {
                            System.out.println("title: " + jp.get("items.related.title[0][" + j + "]"));
                            System.out.println("Tablets: " + jp.get("items.related.permittedDeviceTypes.Tablets[0][" + j + "]"));
                            System.out.println("Handsets: " + jp.get("items.related.permittedDeviceTypes.Handsets[0][" + j + "]"));
                            System.out.println("ConnectedDevices: " + jp.get("items.related.permittedDeviceTypes.ConnectedDevices[0][" + j + "]"));
                            System.out.println("Computers: " + jp.get("items.related.permittedDeviceTypes.Computers[0][" + j + "]"));
                            System.out.println("Duration: " + jp.get("items.related.duration[0][" + j + "]"));
                            System.out.print("\n");
                        }
                    }
                }
            }
        }
    }

AllApiTest.java

  • Class này được xem như là 1 test suite, đảm nhận việc run tất cả các test cases đã được defined ở 2 class Example1Test và Example2Test
    package TestSuite;
     
    import org.junit.runner.RunWith;
    import org.junit.runners.Suite;
    import ApiTests.UseCase1Test;
    import ApiTests.UseCase2Test;
     
     
    @RunWith(Suite.class)
    @Suite.SuiteClasses({
            UseCase1Test.class,
            UseCase2Test.class,
    })
    public class AllApiTest {
    }

4. Execute Test Cases

Sau khi đã code xong, việc cuối cùng chúng ta cần làm là thực hiện chạy các test cases, và chúng ta có thể làm việc này bằng 2 cách

- Cách 1: Dùng command line

Ở trong file pom.xml mà mình đã đề cập ở trên, ở node <profiles> mình đã define AllApiTest.java làm main test suite. Do vậy khi run bằng command line thì java sẽ run file này trước tiên

1. Bật CMD
2. cd vào root directory của source code
3. Run command sau: mvn test –PallApiTests

  • Cách 2: Run từ IntelliJ hoặc Eclipse

Lời kết

Vậy là trong tutorial này, mình đã hướng dẫn các bạn làm như thế nào để có thể thực hiện kiểm thử các api endpoint. Hy vọng là bài viết này sẽ giúp các bạn có cái nhìn tổng quan về api testing và có thể áp dụng được trong các dự án thực tế mà các bạn đang làm.

Các bạn có thể tham khảo source code của mình tại đây

Tham khảo: swtestacadamy.com