Yandex membuka Testsuite



Hari ini kami membuka kode sumber untuk testsuite, kerangka kerja untuk menguji layanan HTTP, yang dikembangkan dan digunakan oleh Yandex.Taxi. Sumber diterbitkan di GitHub di bawah lisensi MIT.

Dengan testuite, mudah untuk menguji layanan HTTP. Ini menyediakan mekanisme siap pakai untuk:

  • HTTP API.
  • HTTP-, .
  • , .
  • , .


Backend Yandex.Taxi terdiri dari ratusan layanan microser, yang baru terus muncul. Kami mengembangkan semua layanan yang sangat dimuat dalam C ++ menggunakan kerangka kerja pengguna kami sendiri, kami sudah membicarakannya di Habré . Kami membuat layanan dan prototipe yang lebih ringan dengan Python.

Untuk memastikan bahwa layanan menyelesaikan masalahnya dengan baik, menyediakan API ke layanan lain dan aplikasi akhir, kami ingin mengujinya secara keseluruhan, terutama berdasarkan prinsip kotak hitam.

Tidak ada alat yang siap pakai untuk ini - Anda harus menulis kode untuk mengatur lingkungan pengujian, yang akan menjadi:

  • meningkatkan dan mengisi basis data;
  • mencegat dan memalsukan permintaan HTTP;
  • menjalankan layanan pengujian di lingkungan ini.

Memecahkan masalah ini menggunakan kerangka kerja untuk pengujian unit terlalu sulit dan salah, karena tugas mereka berbeda: pengujian unit unit struktural yang lebih kecil - komponen, kelas, fungsi.

Testsuite didasarkan pada pytest , kerangka uji Python standar. Tidak masalah apa bahasa dari layanan microser yang kami uji ini ditulis. Sekarang testuite berjalan pada GNU / Linux, sistem operasi macOS.

Meskipun testuite cocok untuk skenario integrasi, yaitu interaksi beberapa layanan (dan jika layanan ditulis dengan Python, maka untuk yang tingkat rendah), kami tidak akan mempertimbangkan kasus ini. Lebih lanjut, kami hanya akan fokus pada pengujian satu layanan.

Tingkat detailAlat uji
Metode / Fungsi, Kelas, Komponen, PerpustakaanTes unit standar, pytest , Googletest , terkadang masih testuite
Layanan microsertestuite
Ensemble Microservice (aplikasi)Tes integrasi Testsuite (tidak tercakup dalam artikel ini)

Prinsip operasi


Tujuan utamanya adalah untuk memastikan bahwa layanan menjawab panggilan HTTP dengan benar, jadi kami menguji melalui panggilan HTTP.

Memulai / menghentikan layanan adalah operasi rutin. Karena itu, kami memeriksa:

  • bahwa setelah memulai layanan merespons melalui HTTP;
  • bagaimana layanan berperilaku jika layanan eksternal sementara tidak tersedia.




Testsuite:

  • Mulai basis data (PostgreSQL, MongoDB ...).
  • Sebelum setiap tes, ini mengisi database dengan data uji.
  • Mulai layanan mikro yang diuji dalam proses terpisah.
  • Meluncurkan server webnya sendiri (mockserver), yang meniru (mengering) lingkungan eksternal untuk layanan.
  • Melakukan tes.

Tes dapat memeriksa:

  • Apakah layanan menangani permintaan HTTP dengan benar.
  • Bagaimana layanan ini bekerja secara langsung dalam database.
  • Ada / tidaknya / urutan panggilan ke layanan eksternal.
  • Keadaan internal layanan menggunakan informasi yang diteruskan ke Testpoint.

mockserver


Kami menguji perilaku satu layanan mikro. Panggilan ke API HTTP layanan eksternal harus dijembatani. Untuk bagian pekerjaan ini di testuite, temui plug-in mockserverdan mockserver_https. Mockserver adalah server HTTP dengan penangan permintaan yang dapat disesuaikan untuk setiap tes dan memori tentang permintaan mana yang diproses dan data apa yang ditransfer.

Basis data


Testsuite memungkinkan tes untuk secara langsung mengakses database untuk membaca dan menulis. Dengan menggunakan data, Anda dapat merumuskan prasyarat untuk pengujian dan memeriksa hasilnya. Out of the box mendukung PostgreSQL, MongoDB, Redis.

Cara mulai menggunakan


Untuk menulis tes testuite, pengembang harus mengetahui Python dan kerangka pytest standar .

Mari kita tunjukkan menggunakan testuite langkah demi langkah menggunakan contoh obrolan sederhana. Berikut adalah kode sumber untuk aplikasi dan tes.



Frontend chat.html berinteraksi dengan layanan chat-backend .

Untuk menunjukkan bagaimana layanan berinteraksi, chat-backend mendelegasikan penyimpanan pesan ke layanan repositori. Penyimpanan diimplementasikan dalam dua cara, chat-storage-mongo dan chat-storage-postgres .

obrolan backend


Layanan chat-backend adalah titik masuk untuk permintaan dari front-end. Mampu mengirim dan mengembalikan daftar pesan.

Layanan


Kami menunjukkan contoh penangan permintaan POST /messages/retrieve:

Kode sumber

@routes.post('/messages/retrieve')
async def handle_list(request):
async with aiohttp.ClientSession() as session:
    #     
    response = await session.post(
        storage_service_url + 'messages/retrieve',
            timeout=HTTP_TIMEOUT,
        )
        response.raise_for_status()
        response_body = await response.json()

        #    ,      
        messages = list(reversed(response_body['messages']))
        result = {'messages': messages}
        return web.json_response(result)

Tes


Persiapkan infrastruktur testuite untuk peluncuran layanan. Kami menunjukkan dengan pengaturan apa kami ingin memulai layanan.

Sumber

#      . 
#       ( scope='session'),   
@pytest.fixture(scope='session')
async def service_daemon(
        register_daemon_scope, service_spawner, mockserver_info,
):
    python_path = os.getenv('PYTHON3', 'python3')
    service_path = pathlib.Path(__file__).parent.parent
    async with register_daemon_scope(
            name='chat-backend',
            spawn=service_spawner(
                #   .    —  ,
                #    
                [
                    python_path,
                    str(service_path.joinpath('server.py')),
                    '--storage-service-url',
                    #       mockserver,
                    #         mockserver   /storage
                    mockserver_info.base_url + 'storage/',
                ],
                #  URL,      
                check_url=SERVICE_BASEURL + 'ping',
            ),
    ) as scope:
        yield scope

Kami mengatur fixture klien, melalui itu tes mengirimkan permintaan HTTP ke layanan.

Sumber

@pytest.fixture
async def server_client(
        service_daemon, # HTTP-  == 204
        service_client_options,
        ensure_daemon_started,
        #   mockserver ,      ,
        #    ,    
        mockserver,
):
    await ensure_daemon_started(service_daemon)
    yield service_client.Client(SERVICE_BASEURL, **service_client_options)

Sekarang infrastruktur tahu bagaimana memulainya chat-backenddan bagaimana cara mengirim permintaan. Ini cukup untuk memulai tes menulis.

Harap dicatat bahwa dalam pengujian chat-backendkami tidak menggunakan layanan penyimpanan, baik chat-storage-mongo, maupun chat-storage-postgres. Untuk chat-backendmenangani panggilan secara normal, kami membasahi API penyimpanan mockserver.

Mari kita menulis tes untuk metode ini POST messages/send. Kami memverifikasi bahwa:

  • permintaan diproses secara normal;
  • saat memproses permintaan, chat-backendpanggil metode penyimpanan POST messages/send.

Sumber

async def test_messages_send(server_client, mockserver):
    #    mockserver   POST messages/send
    @mockserver.handler('/storage/messages/send')    
    async def handle_send(request):
        # ,       ,
        #     chat-backend
        assert request.json == {
            'username': 'Bob',
            'text': 'Hello, my name is Bob!',
        }
        return mockserver.make_response(status=204)

    #    chat-backend
    response = await server_client.post(
        'messages/send',
        json={'username': 'Bob', 'text': 'Hello, my name is Bob!'},
    )
    
    # ,        HTTP-
    assert response.status == 204

    # ,  chat-backend       POST messages/send
    assert handle_send.times_called == 1

Mari kita menulis tes untuk metode ini POST messages/retrieve. Kami memverifikasi bahwa:

  • permintaan diproses secara normal;
  • saat memproses permintaan, chat-backendpanggil metode penyimpanan POST /messages/retrieve;
  • chat-backend “Membalik” daftar pesan yang diterima dari repositori sehingga pesan-pesan terbaru ada di akhir daftar.

Sumber

async def test_messages_retrieve(server_client, mockserver):
    messages = [
        {
            'username': 'Bob',
            'created': '2020-01-01T12:01:00.000',
            'text': 'Hi, my name is Bob!',
        },
        {
            'username': 'Alice',
            'created': {'$date': '2020-01-01T12:02:00.000'},
            'text': 'Hi Bob!',
        },
    ]

    #    mockserver   POST messages/retrieve
    @mockserver.json_handler('/storage/messages/retrieve')
    async def handle_retrieve(request):
        return {'messages': messages}

    #    chat-backend
    response = await server_client.post('messages/retrieve')

    # ,        HTTP-
    assert response.status == 200

    body = response.json()
    
    # ,    chat-backend    ,
    #   ,       
    assert body == {'messages': list(reversed(messages))}

    # ,  chat-backend       POST messages/retrieve
    assert handle_retrieve.times_called == 1


chat-storage-postgres



Layanan chat-storage-postgresini bertanggung jawab untuk membaca dan menulis pesan obrolan ke database PostgreSQL.

Layanan


Ini adalah bagaimana kami membaca daftar pesan dari PostgreSQL dalam metode POST /messages/retrieve:

Source Code

@routes.post('/messages/retrieve')
    async def get(request):
        async with app['pool'].acquire() as connection:
            records = await connection.fetch(
                'SELECT created, username, "text" FROM messages '
                'ORDER BY created DESC LIMIT 20',
            )
        messages = [
            {
                'created': record[0].isoformat(),
                'username': record[1],
                'text': record[2],
            }
            for record in records
        ]
        return web.json_response({'messages': messages})

Tes


Layanan yang kami uji menggunakan database PostgreSQL. Agar semuanya berfungsi, yang perlu Anda lakukan adalah memberi tahu testsuite di direktori mana untuk mencari skema tabel.

Sumber

@pytest.fixture(scope='session')
def pgsql_local(pgsql_local_create):
    # ,     
    tests_dir = pathlib.Path(__file__).parent
    sqldata_path = tests_dir.joinpath('../schemas/postgresql')
    databases = discover.find_databases('chat_storage_postgres', sqldata_path)
    return pgsql_local_create(list(databases.values()))

Pengaturan infrastruktur conftest.py lainnya tidak berbeda dengan layanan yang dijelaskan di atas chat-backend.

Mari kita beralih ke tes.

Mari kita menulis tes untuk metode ini POST messages/send. Periksa apakah itu menyimpan pesan ke database.

Sumber

async def test_messages_send(server_client, pgsql):
    #   POST /messages/send
    response = await server_client.post(
        '/messages/send', json={'username': 'foo', 'text': 'bar'},
    )

    # ,    
    assert response.status_code == 200

    # ,     JSON    
    data = response.json()
    assert 'id' in data

    #     PostgreSQL  
    cursor = pgsql['chat_messages'].cursor()
    cursor.execute(
        'SELECT username, text FROM messages WHERE id = %s', (data['id'],),
    )
    record = cursor.fetchone()

    # ,          , 
    #     HTTP-
    assert record == ('foo', 'bar')

Mari kita menulis tes untuk metode ini POST messages/retrieve. Periksa apakah ia mengembalikan pesan dari database.

Pertama, buat skrip yang menambahkan catatan yang kita butuhkan ke tabel. Testsuite akan secara otomatis menjalankan skrip sebelum pengujian.

Sumber

--  chat-storage-postgres/tests/static/test_service/pg_chat_messages.sql
INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:00.0+03', 'foo', 'hello, world!');
INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:01.0+03', 'bar', 'happy ny');

Sumber

#  chat-storage-postgres/tests/test_service.py
async def test_messages_retrieve(server_client, pgsql):
    #     testsuite     
    #  pg_chat_messages.sql
    response = await server_client.post('/messages/retrieve', json={})
    assert response.json() == {
        'messages': [
            {
                'created': '2019-12-31T21:00:01+00:00',
                'text': 'happy ny',
                'username': 'bar',
            },
            {
                'created': '2019-12-31T21:00:00+00:00',
                'text': 'hello, world!',
                'username': 'foo',
            },
        ],
    }

Meluncurkan


Contoh menjalankan paling mudah dalam wadah buruh pelabuhan. Untuk melakukan ini, Anda perlu membuat docker dan docker-compose yang diinstal pada mesin.

Semua contoh diluncurkan dari direktori Mulai obrolan .docs/examples



#   MongoDB
docs/examples$ make run-chat-mongo

#   PostgreSQL
docs/examples$ make run-chat-postgres

Setelah peluncuran, URL akan ditampilkan di konsol, tempat Anda dapat membuka obrolan di browser:

chat-postgres_1 | ======== Running on http://0.0.0.0:8081 ========
chat-postgres_1 | (Press CTRL+C to quit)

Jalankan tes

#    
docs/examples$ make docker-runtests

#    
docs/examples$ make docker-runtests-mockserver-example
docs/examples$ make docker-runtests-mongo-example
docs/examples$ make docker-runtests-postgres-example

Dokumentasi


Dokumentasi testuite terperinci tersedia di sini .

Petunjuk untuk menyiapkan dan menjalankan contoh.

Jika Anda memiliki pertanyaan github.com/yandex/yandex-taxi-testsuite/issues - tinggalkan komentar.

All Articles