A Path

API Reply validation with Python

In a previous post we did created some tests with hypothesis to generate input date for us. In that post, there was a validation class to validate the password. I check the reply of an API call in every test. API reply validation with python is not difficult. I explain it.

Suppose we have an API that returns some user data. I can for example call a function getPerson with an ID and retrieve the following data:

{
  "name": "testuser",
  "age": 34,
  "interest": "testing"
}

The reply is a json file with some fields. Name should be at least 8 characters and no more that 12 characters. The age is an integer. Users in the system have to be at least 21 years old, so an age of 18 is invalid. The value of interest is one of the following values: “testing”, “programming”, “agile”.

Now I can check all the values with this some python code:


class ReplyValidator(object):
    def check_name(self, name):
        if len(name) < 8:
            return False
        elif len(name) > 12:
            return False
        return True

    def check_age(self, age):
        if age < 21:
            return False
        return True
    
    def check_interest(self, interest):
        if interest not in  ["testing", "programming", "agile"]:
            return False
        return True

class Reply():
    def __init__(self, data):
        self.name = data["name"]
        self.age = data["age"]
        self.interest = data["interest"]
    
    @property
    def is_valid(self):
        return all([
            ReplyValidator().check_name(self.name), 
            ReplyValidator().check_street(self.street),
            ReplyValidator().check_age(self.age),
            ReplyValidator().check_interest(self.interest)
            ])

I created some unit test to test our verification classes too. I still am a tester, so I do test my own code too.

from line import Reply

def test_a_valid_reply():
    reply_data = dict(name="testuser",
                    street="testing street",
                    age=34,
                    interest="testing")

    reply = Reply(reply_data)
    assert reply.is_valid

def test_an_invalid_name():
    reply_data = dict(name="test",
                    street="testing street",
                    age=34,
                    interest="testing")

    reply = Reply(reply_data)
    assert not reply.is_valid

def test_an_invalid_age():
    reply_data = dict(name="test",
                    street="testing street",
                    age=10,
                    interest="testing")

    reply = Reply(reply_data)
    assert reply.is_valid

Use abstract methods in python

The code is very simple. But in case there is an extra validation you have to create the validation and add an extra check. Would it not be nice that you do not have to write a lot of code to check it? It is possible to write a validation class with the descriptor technique.

from abc import ABC, abstractmethod

class BaseValidator(ABC):
    @abstractmethod
    def validate(self, value):
        pass

    def __init__(self):
        self.value = None

    def __get__(self, obj, objtype):
        return self.value
    
    def __set__(self,obj, value):
        self.validate(value)
        self.value = value

First we create a BaseValidator class that inherits from the ABC class. So I can create an abstract method. All classes that inherits from this class have to implement a validation function.

A descriptor can bind a variable like the @property decorator in python. We can create some data classes to hold the values and check if the value is valid. The setter of the descriptor calls first a function to validate the input. All child classes of BaseValidator must implement this function. The interface changed now. There is an exception raised in case the validation failed.

class OneOf(BaseValidator):
    def __init__(self, values=list()):
        self.values = values

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'{value} should be of type str')
        if not(value in self.values):
            raise ValueError(f'{value} should be one of {self.values}')

class String(BaseValidator):
    def __init__(self, min_len = 8, max_len=12):
        self.min_len = min_len
        self.max_len = max_len

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'{value} should be of type str')
        if len(value) < self.min_len:
            raise ValueError(f'length of {value} should be at least {self.min_len}')
        if len(value) > self.max_len:
            raise ValueError(f'length of {value} should be not bigger than {self.max_len}')

class Integer(BaseValidator):
    def __init__(self, minimum=21):
        self.minimum = minimum
    
    def validate(self, value):
        if not isinstance(value, int):
            raise TypeError(f'{value} should be of type int')
        if value < self.minimum:
            raise ValueError(f'{value} should be at least {self.minimum}')

I created 3 example classes. A String, Integer and a OneOf class. The String class has a minimum and maximum length. The validator function checks if the new value is between minimum and maximum length.

The Integer class has a minimum parameter. The validation function checks if the new value is larger than this minimum parameter.

The OneOf class is a special. It receives a list of possible (string) values. The validation function checks if the new value is one of the possible values.

API reply validation with the validators

The existing code should change now. Let’s try it with a minimal change.

class ReplyValidator(object):
    name = String()
    age = Integer()
    interest = OneOf(["testing", "programming", "agile"])

class Reply():
    def __init__(self, data):
        self.name = data["name"]
        self.age = data["age"]
        self.interest = data["interest"]
    
    @property
    def is_valid(self):
        validator = ReplyValidator()
        validator.name = self.name
        validator.age = self.age
        validator.interest = self.interest
        return True

Two test cases needs an update. The code raises an exception when the input is not valid.

def test_an_invalid_name():
    reply_data = dict(name="test",
                    street="testing street",
                    age=34,
                    interest="testing")

    reply = Reply(reply_data)
    with pytest.raises(ValueError):
        not reply.is_valid

def test_an_invalid_age():
    reply_data = dict(name="testuser",
                    street="testing street",
                    age=10,
                    interest="testing")

    reply = Reply(reply_data)
    with pytest.raises(ValueError):
        reply.is_valid

The code changes again. The is_valid method is not needed anymore. The ‘framework’ validates all data now. It raises an error in case the validation fails. So our code can look like this:

class ReplyValidator(object):
    name = String()
    age = Integer()
    interest = OneOf(["testing", "programming", "agile"])

class Reply():
    def __init__(self, data):
        self.validator = ReplyValidator()
        self.validator.name = data["name"]
        self.validator.age = data["age"]
        self.validator.interest = data["interest"]

The code validates the data without extra checks. The reply of the API can now be validated with the Reply class. Put the real API reply as input parameter. API reply validation with python is not difficult. That is exact what I want for my API tests.

About the author

I currently work as a Test Automation Consultant at b.ignited. Here I work for different clients in different industries to help them start and speed up their testing cycles

I’ve been testing software since 2000 when I became involved in testing telephone applications and hardware. Since then, I’ve been able to expand my experience by testing a variety of embedded, web, mobile and desktop applications. I have used various development methodologies from waterfall to agile.

I consider myself to be a lifelong learner.