Incrementally improving test code

Sun, 07 Jul 2019

Writing an automation test, involves many steps. Your goal is to write a simple test that works and then improve it incrementally. For example, on your first attempt, you may make assumptions on the initial state of the database. Once you know that your test runs and works, then you make the code better.

GET ToDos

Let me see if the GET /todos API works at all. My goal is to see if I can make an API request using Cypress. The test should pass eventhough I am not making any specific test assertions here

    it('trial1 - make a GET request to /todos route', () => {
        cy.request('GET', 'http://localhost:3000/todos' );
    });

After I make an API call, I want to look at its response body. Let me first see how the response looks like. Here I am just printing the response body to the console.

    it('trial2 - when i make a GET reqest to /todos route, i should see the response body', () => {
        cy.request('GET', 'http://localhost:3000/todos' )
        .should((response) => {
            console.log(response.body);    
        })
    });

I also will print the length of the response body and see that it shows me an integer.

    it('trial3 - when i make a GET reqest to /todos route, i should see the length of the response body', () => {
        cy.request('GET', 'http://localhost:3000/todos' )
        .should((response) => {
            console.log(response.body.length);    
        })
    });

I know that GET /todos API returns an array of ToDo items. I know that the database has 2 ToDo items. So the GET /todos API response should be an array of length 2. Here I am using the cy.should command to parse the response body.

    it('trial4 - when i make a GET reqest to /todos route, the response body should have length 2..use cy.should', () => {
        cy.request('GET', 'http://localhost:3000/todos' )
        .should((response) => {
            expect(response.body).to.have.length(2)        
        })
    });

This is the same test as above. But in this trial, I will parse the response body using cy.then instead of cy.should

    it('trial5 - when i make a GET reqest to /todos route, i should see the response body..use cy.then', () => {
        cy.request('GET', 'http://localhost:3000/todos' )
        .then((response) => {
            expect(response.body).to.have.length(2)    
        })
    });

DELETE ToDos

There is one problem with the above tests. Eventhough the tests run fine, they make one big assumption. That is there are 2 ToDo items in the database before the test starts. The problem with this is that if the database has a different number of ToDos or no ToDos at all, my test will fail. I need to make the test more stable. If I delete all the ToDos first, and then do the GET /todos API, it should return me a response with an emtpy array.

Here is my first trial at deleting a single ToDo. Remember that I had 2 ToDos initially. When I delete one ToDo, the GET /todos should return me a response data of length 1.

    it('trial6 - when i delete one item and then do a GET /todos, i should get 1 item', () => {
        cy.request('DELETE', 'http://localhost:3000/todos/1')
        cy.request('GET', 'http://localhost:3000/todos')
        .should((response) => {
            expect(response.body).to.have.length(1)
        })
    });

So now I know deleting 1 ToDo works. Let me try deleting 2 ToDos.

    it('trial7 - when i delete both the items, i should see a response body with zero length', () => {
        cy.request('DELETE', 'http://localhost:3000/todos/1')
        cy.request('DELETE', 'http://localhost:3000/todos/2')
        cy.request('GET', 'http://localhost:3000/todos')
        .should((response) => {
            expect(response.body).to.have.length(0)
        })
    });

In the test above, the DELETE API was done with specific ids..1 and 2. I dont want to hardcode the ids 1 and 2 in my test. It should delete all Todos irrespective of their Ids. First let me see how to print out the Ids of each ToDo.

    it('trial8 - when i do a GET /todos, print out the id property value of each item from the response', () => {
        cy.request('GET', 'http://localhost:3000/todos')
        .should((response) => {
            expect(response.body).to.have.length(2)
            console.log(response.body[0].id)
            console.log(response.body[1].id)

        })
    });

Now that I know how to print the Ids of each ToDo, I can delete the ids by passing in the Id.

    it('trial9 - delete all items by first getting all items and then looping through each item to delete them one by one', () => {
        cy.request('GET', 'http://localhost:3000/todos')
        .its('body')
        .each(($bodyItem) => {
            console.log($bodyItem)
            var idValue = $bodyItem.id
            cy.request('DELETE', 'http://localhost:3000/todos/' + idValue)
        })
    })

Putting it all together

I now know how to delete all ToDos from the database irrespective of the number of ToDos in the database initially. So I will now delete all ToDos and then make the GET /todos API request. The response from the GET API should have an HTTP status of 200, and its body should be an empty array of length zero.

    it('trial10 - two blocks of code...first block deletes all items..then second block gets all the items', () => {
        cy.request('GET', 'http://localhost:3000/todos')
        .its('body')
        .each(($bodyItem) => {
            console.log($bodyItem)
            var idValue = $bodyItem.id
            cy.request('DELETE', 'http://localhost:3000/todos/' + idValue)
        })

        cy.request('GET', 'http://localhost:3000/todos')
        .should((response) => {
            expect(response.body).to.have.length(0)
            expect(response.status).to.equal(200)
        })
    })

Now let me make the test better. The delete operations that were done to delete all the ToDos can be moved to a separate function, say deleteAllItems. Similarly the GET operation can be moved to its own function ..say getItems. By creating these two functions, I can reuse them in other test cases too.

    function deleteAllItems() {
        cy.request('GET', 'http://localhost:3000/todos')
        .its('body')
        .each(($bodyItem) => {
            console.log($bodyItem)
            var idValue = $bodyItem.id
            cy.request('DELETE', 'http://localhost:3000/todos/' + idValue)
        })
    }

    function getItems() {
        return cy.request('GET', 'http://localhost:3000/todos')        
    }

    it('trial11 -this test calls two functions..first function deletes all items..second function gets all the items', () => {
        
        deleteAllItems()
        getItems()
        .should((response) => {
            expect(response.body).to.have.length(0)
            expect(response.status).to.equal(200)
        })

    })

The source code for these tests are in https://github.com/swiftparrot/todos-experiments

Loading...