# Unit testing Zato services

Unit tests for services in your IDE, the command line and CI/CD pipelines, with no server running.

- You can unit test your Zato services directly in your IDE, as well as from the command line, or with automated testing in CI/CD pipelines, all without having to run a Zato server

- You do it using the [zato-testing](https://pypi.org/project/zato-testing/) framework, and in this tutorial you will learn how to simulate external connections, invoke services, and verify their responses

- This tutorial will get you started, and separate chapters cover testing [REST](https://zato.io/docs/api-testing/rest.html), [SQL](https://zato.io/docs/api-testing/sql.html), [Microsoft 365](https://zato.io/docs/api-testing/ms365/index.html), [LDAP](https://zato.io/docs/api-testing/ldap.html), [Jira](https://zato.io/docs/api-testing/jira.html), more advanced [service invocation scenarios](https://zato.io/docs/api-testing/services.html), as well as [CI/CD integrations](https://zato.io/docs/api-testing/ci-cd.html)

## Getting started {#getting-started}

- Clone the [DevOps blueprint project](https://github.com/zatosource/zato-project-blueprint) - it has everything set up and ready to use
- Open it in VS Code
- The blueprint already comes with the testing framework installed and configured, so you don't need to do anything else to follow this tutorial

## Your first unit test {#your-first-unit-test}

The blueprint has this sample service which invokes a CRM via REST and returns some data from it (in ./myproject/impl/src/api/crm.py):

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetCustomer(Service):
    """ Returns customer details from the CRM system.
    """
    name = 'crm.customer.get'

    input = 'customer_id'
    output = 'name', 'email'

    def handle(self):

        # Get the CRM connection ..
        conn_name = 'crm.api'
        conn = self.rest[conn_name]

        # .. the data to be sent ..
        params = {'id': self.request.input.customer_id}

        # .. invoke the CRM ..
        response = conn.get(params)

        # .. and build our response.
        self.response.payload.name = response.data['name']
        self.response.payload.email = response.data['email']
```

And here's the test for that service (in ./myproject/testing/tests/test\_crm.py):

```python
# -*- coding: utf-8 -*-

# zato-testing
from zato_testing import ServiceTestCase

# Our code
from api.crm import GetCustomer

class TestGetCustomer(ServiceTestCase):

    def test_returns_customer_details(self):

        # Prepare test data ..
        name = 'Alice Johnson'
        email = 'alice@example.com'
        customer_id = 'CUST-001'

        # .. connection the service uses ..
        conn_name = 'crm.api'

        # .. tell the framework to make use of that data ..
        self.set_response(conn_name, {
            'name': name,
            'email': email,
        })

        # .. invoke the service ..
        service = self.invoke(GetCustomer, {'customer_id': customer_id})

        # .. and run our assertions.
        self.assertEqual(service.response.payload.name, name)
        self.assertEqual(service.response.payload.email, email)
```

## Running tests in VS Code {#running-tests-in-vs-code}

Open the blueprint in VS Code, go to the Testing panel, point it to zato-project-blueprint (use test\_\*.py for the pattern it'll ask about), and click the play button. The test will run and show the result.

## How does it work? {#how-does-it-work}

The test framework creates a test environment that reproduces Zato's runtime. When your services invoke external systems, using REST, SQL and similar, these calls are intercepted and, instead of actually making calls to any systems, the framework returns the responses you configured with **self.set\_response**.

From your service's perspective, it doesn't realize anything. Its own code doesn't change, it's the framework that does the job, based on what you tell it via set\_response.

So, in the example below, we told set\_response to return a specific dictionary of data each time "crm.api" gets invoked, and then we invoke that service.

Next, the service invoked "crm.api", the framework intercepted that call, figured out that "crm.api" is a REST outgoing connection, and returned to the service the dictionary you passed to set\_response.

The service then proceeded exactly as it would after a call to a real REST endpoint, so it used the dictionary to build a final response, assigned to self.response.payload.

Back to your unit test, you can now create assertions about the response from that service. In this case, we're just confirming that the values equal the expected ones, but you can use anything from the Python's built-in [unittest](https://docs.python.org/3/library/unittest.html) library or from any other Python testing library.

Naturally, set\_response has other options as well, and that is what the remainder of this tutorial will be about.

And lastly, once you get the hang of how the testing framework works, it's best that you [point your AI to Zato documentation](https://zato.io/docs/ai/connect-your-copilot.html) and tell it to generate the tests for you.

## But wait, how to run the tests outside VS Code? {#but-wait-how-to-run-the-tests-outside-vs-code}

- In the blueprint project, you can run the **./myproject/impl/scripts/run-tests.sh** script to execute the tests from the command line:

  ```bash
     $ ./myproject/impl/scripts/run-tests.sh
      test_returns_customer_details (TestGetCustomer.test_returns_customer_details) ... ok

      ----------------------------------------------------------------------
      Ran 1 test in 0.002s

      OK
      $
  ```

- You can also run your tests from [CI/CD pipelines](https://zato.io/docs/api-testing/ci-cd.html), such as Azure DevOps or GitHub Actions.

- Finally, you should always base your projects on the blueprint, but there may be more advanced scenarios, such as when you want to have a dedicated repository for tests only, as opposed to keeping tests and services in the same place.

  To handle this case, you'll simply add [zato-testing](https://pypi.org/project/zato-testing/) to your requirements.txt. It's just a Python library, without any dependencies, which you can install into any project.

## How to use self.set\_response {#how-to-use-selfset_response}

The **self.set\_response** method configures what your service receives when it calls an external connection or if it calls another service.

Note that the same set\_response method is used to provide responses for all the possible connection types, e.g. there is no set\_rest\_response, set\_sql\_response, etc. This is possible because there is typically no overlap between connection names, so usually there is no SQL connection called "crm.api" along with a REST connection of the same name.

It means that you can just use the names of the connections as they are with set\_response, and the framework will in runtime know which actual type a give name refers to. If, however, you have such conflicting names, it is possible to resolve the conflicts, and it's documented below as well.

Also note that you can call set\_response multiple times, this is sometimes handy, e.g. to configure different responses depending on different request data.

Now, let's go through each of the ways set\_response can be used.

### Basic usage {#basic-usage}

Return a dictionary on every call to a connection, e.g. no matter how many times you cann "crm.api" the same dictionary will be returned.

```python
self.set_response('crm.api', {'name': 'Alice', 'email': 'alice@example.com'})
```

### Specifying an HTTP method {#specifying-an-http-method}

By default, responses are for GET requests. Use `method` for other HTTP methods. Now, a different response will be returned by calls to "crm.api", depending on whether your service uses POST vs. DELETE with self.rest.

```python
self.set_response('crm.api', {'id': '123'}, method='POST')
self.set_response('crm.api', {'deleted': True}, method='DELETE')
```

### Setting a status code {#setting-a-status-code}

You can return a specific HTTP status code along with a response.

```python
self.set_response('crm.api', {'error': 'Not found'}, status_code=404)
self.set_response('crm.api', {'error': 'Server error'}, status_code=500)
```

### Setting response headers {#setting-response-headers}

You can tell the framework to include specific headers in the response:

```python
self.set_response('crm.api', {'data': '...'}, headers={'X-Request-Id': 'abc123'})
```

### Multiple calls to the same connection {#multiple-calls-to-the-same-connection}

When your service calls the same connection multiple times, provide a list of responses, and each call returns the next item. If the framework runs out of responses to return, it'll raise an exception.

So, in the example below, you are providing 3 dictionaries, when your service calls "crm.api" the first time, the first dictionary will be used, and so on.

What happens if you call it 4 times? You'll get an exception pointing this fact out to you.

```python
self.set_response('crm.api', [
    {'page': 1, 'items': ['a', 'b']},
    {'page': 2, 'items': ['c', 'd']},
    {'page': 3, 'items': []}
])
```

### Matching responses to specific requests {#matching-responses-to-specific-requests}

Sometimes it's required to return different responses depending on what the request data is. Simply call set\_response multiple times, using the **request** argument, and the framework will understand to return different responses depending on which request is sent from services.

```python
self.set_response('crm.api', {'type': 'premium'}, request={'customer_id': 'CUST-001'})
self.set_response('crm.api', {'type': 'standard'}, request={'customer_id': 'CUST-002'})
```

Moreover, multiple requests can map to the same response:

```python
self.set_response('crm.api', {'status': 'active'}, request=[
    {'customer_id': 'CUST-001'},
    {'customer_id': 'CUST-002'}
])
```

### Connection types and name conflicts {#connection-types-and-name-conflicts}

If you don't have any conflicts in names, set\_response\` will know itself which connection type such and such name actually is.

If you have connections of different types with the same name (e.g., a REST connection and an LDAP connection both named `crm.api`), prefix the name with the connection type:

```python
self.set_response('rest:crm.api', {'data': '...'})
self.set_response('ldap:crm.api', [{'cn': 'John Doe', 'mail': 'john@example.com'}])
```

Here are all the possible prefixes:

- rest
- ldap
- sql
- jira
- service

### Using with self.invoke and self.invoke\_async {#using-with-selfinvoke-and-selfinvoke_async}

When your service calls other services using self.invoke or self.invoke\_async, you can use the same set\_response method to simulate their responses.

For instance, if any other service invokes the services below, it will get a given dictionary on response and the real services won't be invoked at all, which means invoked services are mocked with the same set\_response call as REST, SQL and other connection types.

```python
self.set_response('my.service.get-client', {'name': 'Alice'})
self.set_response('my.service.get-billing', {'balance': 100})
```

Note that for self.invoke\_async, the framework runs the service synchronously during tests (to keep them deterministic) but returns an async correlation ID as it would in production.

## The workflow again {#the-workflow-again}

1. **Write your service** - Your service calls external APIs, queries databases, invokes other services. You don't write any special test code. The production code is the test code.

   ```python
   class GetCustomer(Service):
       def handle(self):
           response = self.rest['crm.api'].get()
           self.response.payload = response.data
   ```

2. **Create a test class** - Extend ServiceTestCase and call self.set\_response for each external connection. The framework figures out the connection type based on how your service uses it.

   ```python
   class TestGetCustomer(ServiceTestCase):
       def test_returns_customer(self):
           self.set_response('crm.api', {'name': 'Alice'})
   ```

3. **Invoke your service** - Call self.invoke with your service class and input data. The framework wires up mock responses and runs your service's handle method.

   ```python
           service = self.invoke(GetCustomer, customer_id='CUST-001')
   ```

4. **Assert on results** - The invoked service is returned to you, so you can check the response payload or any state your service modified.

   ```python
           self.assertEqual(service.response.payload['name'], 'Alice')
   ```

## Learn more {#learn-more}

- [API testing](https://zato.io/docs/api-testing/) - Tests in plain English, run against real or mocked connections
- [Testing REST connections](https://zato.io/docs/api-testing/rest.html) - Mock outgoing REST calls and assert on what your service sent
- [Testing SQL connections](https://zato.io/docs/api-testing/sql.html) - Mock database queries and the rows they return
- [Testing LDAP connections](https://zato.io/docs/api-testing/ldap.html) - Mock directory lookups and the entries they find
- [Testing Microsoft 365](https://zato.io/docs/api-testing/ms365/) - Mock mailboxes, calendars, SharePoint, Teams and the rest
- [Testing service invocations](https://zato.io/docs/api-testing/services.html) - Services that call other services, with the callees mocked
- [Testing Jira connections](https://zato.io/docs/api-testing/jira.html) - Mock Jira API calls and the issues they return
- [Tests in CI/CD](https://zato.io/docs/api-testing/ci-cd.html) - Running the suite in a pipeline and reading its output
