Let’s look at the essence: Why do microservices need contract testing?

01. Why do microservices need contract testing

First let me introduce the company. We use a microservice architecture, and each department is responsible for the development and maintenance of several microservices. My department maintains the company’s payment service (billing), which relies on several services from other departments.

When a user needs to pay for an order, the billing service will be called, carrying many parameters. For convenience, we only consider the two core parameters: user id and payment amount.

When billing receives a user request, it will call other dependent services, and the user service (user) is one of them. We need to check whether the user has enough balance to pay for the order.

Picture

If you want to learn automated testing, I recommend a set of videos to you. This video can be said to be the number one automated testing tutorial on the entire network played by Bilibili. The number of people online at the same time has reached 1,000, and there are also notes available for collection. Technical communication with various experts: 798478386

[Updated] A complete collection of the most detailed practical tutorials on Python interface automation testing taught by Bilibili (the latest practical version)_bilibili_bilibili [Updated] A complete collection of the most detailed practical tutorials on Python interface automated testing taught by Bilibili (practical version) The latest version) has a total of 200 videos, including: 1. Why interface automation should be done for interface automation, 2. Overall view of request for interface automation, 3. Interface practice for interface automation, etc. For more exciting videos from UP master, please follow the UP account . icon-default.png?t=N7T8https://www.bilibili.com/video/BV17p4y1B77x/?spm_id_from=333.337

def pay(uid, amount):
    """Payment"""  
    user_host = app.config.get("user_server_host")
    user_server = f'{user_host}/user/{uid}/pay'
    resp = httpx.get(user_server)
    user = resp.json()
    if user.get('balance') < amount:
      return {"msg": "not enough money"}
    return {"msg": "success", "amount": amount, "uid": uid}

User is called by many services, and billing mainly wants to consume its balance field to verify whether the balance is sufficient. User determines if it is the /user/ interface requested by billing, and then gives it the balance. The data returned by other consumer requests is different.

def user_info(uid, src):
    """User Info"""  
if src == 'pay':
    return {"id": uid,"balance": 8000}
elif src == 'manage':
    return { "id": uid, "age": 19}
...

One day, users reported directly that there was a payment error, but we have not made any modifications to the billing service recently. Obviously, this emergency may not have been caused by our department.

We could only urgently go online to troubleshoot the problem. After a series of analyses, we finally found that the balance field returned by the user service had been changed from balance to user_balance, and no other changes had occurred. The user service modified the code without notifying us.

Slight changes in the data structure result in us not being able to get the balance field at all, so our payment service cannot continue.

Picture

So I could only send an email to the user department immediately. In the maintenance process of microservices, the simplest and most effective testing tool is email. Because a service often depends on multiple other services and is dependent on other services, the final data flow becomes like a spider web. But when it comes to cross-department collaboration, communication is not that easy.

Let’s take a look at the process involved in such a simple change. First, the user service receives a requirement change. After modifying the code, user’s testers will conduct component testing on their own service. However, they have no way to test billing’s call to user’s integration test because they have no idea how billing consumes the data it provides. of.

user decided to notify all downstream services. Anyone who uses my service and this interface will send an email. Please ask your respective departments to test your business. My /user/ interface has been modified. It may cause your service to be abnormal. After other departments receive the notification, they will test this interface in a targeted manner.

These downstream departments will waste a lot of time and resources just because of a small change. why? The billing service is affected by the modification and is therefore necessary for us. However, although other services call the /user/ interface, they do not consume the balance field. Changes to the balance field will not have any impact on them. But they don’t know whether there will be any impact before testing. Is it a waste of time?

One more question, can’t the billing department set up an integration test for the user service? Of course. In fact, we have tests specifically for mock user services, as well as integration tests for users, and occasionally manual tests.

However, testing methods such as mock testing, integration testing and manual testing cannot detect problems in time.

First let’s look at mock testing. The mock server is controlled by ourselves. Its results are not trustworthy and cannot completely replace the real service. The reason why we use mock is because it is fast, controllable, highly stable, and can achieve environmental isolation. .

When the real user service changes, the mock data format cannot be updated in time, so these test cases will not become popular, but will continue to pass.

Picture

Both integration testing and manual testing have the same flaw, they are too slow. The entire set of integration tests in the billing department took 5 hours from build to completion, and one round of manual testing took even longer. In other words, it would take at least 5 hours for us to notice that the services we depended on had changed, and this problem had already caused our system to be paralyzed for 5 hours.

Contract testing is a comprehensive testing method that combines mock testing and integration testing. It forms the test cases written by the consumer billing into a contract file (contract file) and puts it on the provider user side to build.

After the code is modified, the provider user first builds a test for all contract files. If all contract files are satisfied and no contract breach occurs, it can go online. If there is a breach of contract, you need to negotiate with the party who has breached the contract to resolve it before going online.

Attached are consumer and provider sample code, as well as mock test and integration test sample code:

billing server:

import httpx
from flask import (Flask,request)
app = Flask(__name__)
app.config.update(user_server_host='<http://localhost:5001>')

@app.route('/pay/<uid>/<int:amount>')
def pay(uid, amount):
    """Payment"""  
    user_host = app.config.get("user_server_host")
    user_server = f'{user_host}/user/{uid}/pay'
    resp = httpx.get(user_server)
    user = resp.json()
    if user.get('balance') < amount:
        return {"msg": "not enough money"}
    return {"msg": "success","amount": amount, "uid": uid }
if __name__ == '__main__':
    app.run(port=5000, debug=True)

user_server:

from flask import (Flask, request)
app = Flask(__name__)
@app.route('/user/<uid>/<src>')
def user_info(uid, src):
"""Payment"""  
if src == 'pay':
    return {"id": uid, "balance": 8000}
elif src == 'manage':
    return {"id": uid, "age": 19}
...
if __name__ == '__main__':
    app.run(port=5001)

Test code:

import unittest
from billing_server import app

class TestBilling(unittest.TestCase):
    def test_mock_user_server(self):
        app.config.update(user_server_host='<http://localhost:5002>')
        with app.test_client() as client:
        resp = client.get('/pay/1/100')
        assert b"amount" in resp.data
    def test_real_user_server(self):
        app.config.update(user_server_host='<http://localhost:5001>')
        with app.test_client() as client:
        resp = client.get('/pay/1/100')
        assert b"amount" in resp.data
if __name__ == '__main__':
    unittest.main()

After the real user service is updated, if the test case on the bill server mocks the dependent user service, the test case will continue to pass because the mock service is not trustworthy.

If you use the integrated testing method and directly call remote services, it will inevitably cause the test to run very slowly. If the entire set of tests takes 2 hours to run, it will cause the user to be unable to use it for 2 hours before the problem is discovered.

The knowledge points of the article match the official knowledge archives, and you can further learn relevant knowledge. Cloud native entry-level skills treeService grid (istio)ServiceMesh introduction 16679 people are learning the system