Rest Assured Tutorial - Learn Rest API Testing (Automation) from Scratch

Rest Assured is one of the most popular libraries which is highly used in API Test Automation in most of the companies. In this Rest Assured tutorial, I will try to explain Rest API, API Testing, API Automation, REST, and SOAP protocols.

Rest Assured Tutorial Outline

In this post, I will explain what is API and API testing, what is the difference between SOAP and REST services, and how to test REST APIs with Rest Assured Library.

What is API?

API stands for Application Programming Interface. It comprises of a set of functions that can be accessed and executed by another software system. Thus, it serves as an interface between different software systems and establishes their interaction and data exchange.

What is API Testing?

In the modern development world, many web applications are designed based on three-tier architecture model. These are:

1) Presentation Tier – User Interface (UI)

2) Logic Tier – Business logic is written in this tier. It is also called Business Tier. (API)

3) Data Tier – Here information and data is stored and retrieved from a Database. (DB)

Ideally, these three layers (tiers) should not know anything about the platform, technology, and structure of each other. We can test UI with GUI testing tools and we can test logic tier (API) with API testing tools. Logic tier comprises of all of the business logic and it has more complexity than the other tiers and the test executed on this tier is called as API Testing.

API testing tests logic tier directly and checks expected functionality, reliability, performance, and security. In the agile development world, requirements are changing during short release cycles frequently and GUI tests are more difficult to maintain according to those changes. Thus, API testing becomes critical to test application logic.

In GUI testing we send inputs via keyboard texts, button clicks, drop-down boxes, etc., on the other hand in API testing we send requests (method calls) to the API and get output (responses). These APIs are generally REST APIs or SOAP web services with JSON or XML message payloads being sent over HTTP, HTTPS, JMS, and MQ.

REST vs SOAP

REST (REpresentational State Transfer)

REST is an architectural style that uses simple HTTP calls for inter-machine communication. REST does not contain an additional messaging layer and focuses on design rules for creating stateless services. A client can access the resource using the unique URI and a representation of the resource is returned. With each new resource representation, the client is said to transfer state. While accessing RESTful resources with HTTP protocol, the URL of the resource serves as the resource identifier and GET, PUT, DELETE, POST and HEAD are the standard HTTP operations to be performed on that resource. [1][2][6]

SOAP (Simple Object Access Protocol)

SOAP relies heavily on XML, and together with schemas, defines a very strongly typed messaging framework. Every operation the service provides is explicitly defined, along with the XML structure of the request and response for that operation. Each input parameter is similarly defined and bound to a type: for example, an integer, a string, or some other complex object. All of this is codified in the WSDL – Web Service Description (or Definition, in later versions) Language. The WSDL is often explained as a contract between the provider and the consumer of the service. SOAP uses different transport protocols, such as HTTP and SMTP. The standard protocol HTTP makes it easier for SOAP model to tunnel across firewalls and proxies without any modifications to the SOAP protocol. [3][4][6]

REST API Testing with Rest Assured

What is Rest Assured?

In order to test REST APIs, I found REST Assured library so useful. It is developed by JayWay Company and it is a really powerful catalyzer for automated testing of REST-services. REST-assured provides a lot of nice features, such as DSL-like syntax, XPath-Validation, Specification Reuse, easy file uploads and with those features we will handle automated API testing much easier.

Rest Assured has a gherkin type syntax which is shown in below code. If you are a fan of BDD (Behavior Driven Development), I believe that you will love this kind of syntax.

@Test
public void exampleRestTest() {
    given()
        .contentType(ContentType.JSON)
        .pathParam("id", "AskJsd8Sd")
    .when()
        .get("/examplepath/{id}")
    .then()
        .statusCode(200)
        .body("firstName", equalTo("Onur"))
        .body("Surname", equalTo("Baskirt"));
}

Also, you can get JSON response as a string and send it to the JsonPath class and use its methods to write more structured tests. I generally prefer JsonPath for more structured tests.

@Test
public void exampleJsonPathTest() {
  Response res = get("/service/example");
  assertEquals(200, res.getStatusCode());
  String json = res.asString();
  JsonPath jp = new JsonPath(json);
  assertEquals("onur@swtestacademy", jp.get("email"));
  assertEquals("Onur", jp.get("firstName"));
  assertEquals("Baskirt", jp.get("lastName"));
}

How to Make a POST Request with RestAssured?

The following code uses requestSpecBuilder to make a post request. Parameter descriptions are listed below.

  • restAPIURL – URL of the Rest API
  • APIBody – Body of the Rest API. Example: {“key1″:”value1″,”key2″:”value2”}
  • setContentType() – Pass the “application/json”, “application/xml” or “text/html” etc. headers to setContenType() method.
  • Authentication credentials – Pass the username and password to the basic() method or if there is no authentication leave them blank basic(“”,””)
@Test
public void httpPostMethod() throws JSONException,InterruptedException {
 
 //Rest API's URL
 String restAPIUrl = "http://{URL of API}";
 
 //API Body
 String apiBody = "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}";
 
 // Building request by using requestSpecBuilder
 RequestSpecBuilder builder = new RequestSpecBuilder();
 
 //Set API's Body
 builder.setBody(apiBody);
 
 //Setting content type as application/json
 builder.setContentType("application/json; charset=UTF-8");
 
 RequestSpecification requestSpec = builder.build();
 
 //Making post request with authentication or leave blank if you don't have credentials like: basic("","")
 Response response = given().authentication().preemptive().basic({username}, {password})
 .spec(requestSpec).when().post(restAPIUrl);
 
 JSONObject JSONResponseBody = new JSONObject(response.body().asString());
 
 //Get the desired value of a parameter
 String result = JSONResponseBody.getString({key});
 
 //Check the Result
 Assert.assertEquals(result, "{expectedValue}");
 
 }

Gherkin Style Rest Assured POST Request

@Test
 public void postExamplebyGherkin()
 {
     RestAssured.baseURI  = "Your API URL"; 
 
     Response res = given()
     .contentType("application/json").
     body("{\"name\":\"Onur Baskirt\"}").
        when().
        post("");
 
     String body = res.getBody().asString();
     System.out.println(body);
 
 }

For more theory, please visit references. I want to go with an example questions and their solutions.

Examples

Examples 1

Test Description: Test a search with a search term and number of 4 videos parameter.

Base URL: http://api.5min.com/

Base path: search

Search term: Barack Obama

Parameter: num_of_videos

URL: http://api.5min.com/search/barack%20obama/videos.json?num_of_videos=4

Expected Result: We will receive details of the URL to the video and associated title and description in JSON format.

Verifications/Tests:

  • Verify the HTTP response status returned.
  • Verify the response contained the relevant search term
  • Verify that only 4 video entries were returned
  • Verify that there is no duplicate video
  • Print video title, pubDate & duration

Examples 2

Test Description: Test a search with an id of video and number of 4 related videos parameter.

Base URL: http://api.5min.com/

Base Path: video

Search term: 519218045

Parameter: num_related_return

URL: http://api.5min.com/video/list/info.json?video_ids=519218045&num_related_return=4

Expected Result: We will receive details of that video and 4 related videos including their title, URL and associated image in JSON format.

Verifications/Tests:

  • Verify the HTTP response status returned.
  • Verify that response was successful and relevant to given video_id
  • Verify that 4 additional video entries were returned along with the given video_id
  • Verify that there is no duplicate in related videos
  • Print video title, permittedDeviceTypes status (for “Tablets”,”Handsets”,”ConnectedDevices”,”Computers”) & time duration

Solutions

Strategy: First, it is very reasonable to use a framework/library which provides us testing an API easily in a short period of time and we chose the Rest-assured library. It is better to write the code with below rules.

Single Responsibility:
– Each test has a single responsibility and includes a single assertion.
Explicit separation has advantages while we are doing black-box testing.

High Level of Abstraction: – The logic of a test should be written in a high-level way.
– All details such as sending request, creating request, dealing with IO should be done via utility methods not to do with inline methods.

Re-usability: Write the class, methods etc. that can be re-used for the testing of other endpoints of API.

Documentation:
Add documentation, comments which describe the tests sufficiently.

Extra Tools:
You can format JSON responses with notepad++’s JSON Viewer plugin.

[Rest Assured

Project Structure:

1) I wrote HelperMethods class and RestUtil classes for reusability and clean project structure. – HelperMethods class comprises of test-related functions. – RestUtil class contains Rest Assured related methods.

[Rest Assured Tutorial

2) Example1Test and Example2Test contain our tests and assertions.

3) AllApiTest is the test runner suite. We can run all the test with the AllApiTest test suit.

In this project, I used Maven and pom.xml is our dependency and profile xml file. We will also go into details of this file further.

Now we can start to examine the test code. We start with pom.xml.

POM.xml

In pom.xml we should add:

JUnit library – It is our test framework

Hamcrest library – For assertion methods

– Jayway Rest Assured library – It is our REST API testing library

– For running the test from command prompt with maven I also added AllApiTests profile at the end of the pom.xml. We can run all tests with “mvn test –PallApiTests” command.

pom.xlm code is shown below:

<?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>

I tried to explain the other codes line by line. Now I want to go on with project’s JAVA files.

RestUtil.java

It is a utility class for Rest Assured Library. It contains many methods that help us to write our codes more effectively.

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

It is another utility class for our example1 and example 2. It contains methods about example1 and example2. We created this class for creating clean project structure and prevent code replication.

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

I explained the code line by line below.

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

I explained the code line by line.

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

It is our test suite. We use this class name to create a profile in pom.xml.

[Rest API Testing

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

Thus, we can run our tests on command prompt by typing below maven command.

mvn test –PallApiTests

Test Execution

When you create your project with the files that are described and shared in this post. **You should go project directory in command prompt and simply type below command. **

mvn test –PallApiTests

Then, our tests will run as shown below.

[API Testing Tutorial

Also, you can run the tests in IntelliJ or Eclipse.

[API Test Automation

Github Link: https://github.com/swtestacademy/RestAssuredExample>

This example API is not working anymore. I wrote a new project for Swagger and you can also check that example here: https://github.com/swtestacademy/api-automation-rest-assured-basic

#testing #api #rest #webdev

Rest Assured Tutorial - Learn Rest API Testing (Automation) from Scratch
11.90 GEEK