功能測試作為全端專案的設計工具。第二部分:API伺服器和專案發布

DDD
發布: 2024-10-13 06:09:30
原創
686 人瀏覽過

在本文的前一部分中,我們實作了Full Stack專案的整體設計。我們還使用功能測試來設計和實作該專案的 Web 部分。我建議重新訪問前一部分,以便更清楚地了解迄今為止所涵蓋的材料。

在這一部分中,我們將應用相同的方法(透過功能測試定義進行設計)到專案的 API 部分及其發布。

為了實作 API 部分,我們將使用 Python(儘管也可以使用任何其他語言)。

Web 零件有何幫助?

實作的 Web 元件提供:

  • 用戶註冊
  • 註冊用戶登入
  • 顯示使用者資訊

如示範的,由於使用了模擬,Web 元件可以在不連接到 API 元件的情況下運作。

這些mocks可以幫助我們定義API部分的詳細用途。

Web 部件 (mocks.ts) 中定義的模擬有:

const mockAuthRequest = async (page: Page, url: string) => {
    await page.route(url, async (route) => {
        if (route.request().method() === 'GET') {
            if (await route.request().headerValue('Authorization')) {
                await route.fulfill({status: StatusCodes.OK})
            }
        }
    })
}

export const mockUserExistance = async (page: Page, url: string) => {
    await mockAuthRequest(page, url)
}

export const mockUserInfo = async (page: Page, url: string, expectedApiResponse: object) => {
    await mockRequest(page, url, expectedApiResponse)
}

export const mockUserNotFound = async (page: Page, url: string) => {
    await mockRequest(page, url, {}, StatusCodes.NOT_FOUND)
}

export const mockServerError = async (page: Page, url: string) => {
    await mockRequest(page, url, {}, StatusCodes.INTERNAL_SERVER_ERROR)
}

export const mockUserAdd = async (page: Page, userInfo: UserInfo, url: string) => {
    await mockRequest(page, url, {}, StatusCodes.CREATED, 'POST')
}

export const mockUserAddFail = async (page: Page, expectedApiResponse: object, url: string) => {
    await mockRequest(page, url, expectedApiResponse, StatusCodes.BAD_REQUEST, 'POST')
}

export const mockExistingUserAddFail = async (page: Page, userInfo: UserInfo, url: string) => {
    await mockRequest(page, url, {}, StatusCodes.CONFLICT, 'POST')
}

export const mockServerErrorUserAddFail = async (page: Page, url: string) => {
    await mockRequest(page, url, {}, StatusCodes.INTERNAL_SERVER_ERROR, 'POST')
}
登入後複製

定義 API 用途與設計

基於開發的 Web 元件,我們概述一下 API 的主要用途(用例):

  • 使用者認證
  • 為系統新增使用者
  • 從系統中刪除使用者
  • 檢索使用者資訊

根據 Web 元件的功能測試,我們得到了 API 的以下端點定義:

  • /user — 支援 GET 和 POST 方法
  • /user_info/${username} — 支援 GET 方法

要實現完整功能,我們應該為 /user 端點新增 DELETE 方法,即使 Web 元件中未使用該方法。

API部分的整體設計如下:

Functional Testing as a Design Tool for Full Stack Projects. Part II: API server and Project Release

我們將使用的工具(儘管可以替換類似的替代品):

  • Falcon — 一個閃電般快速、簡約的 Python Web API 框架,用於建立強大的應用程式後端和微服務
  • Pytest — 一個框架,可簡化編寫小型、可讀的測試和擴展,以支援應用程式和庫的複雜功能測試

檢定定義

我們將按照與上一個相同的方法來實作 API 部分的設計:

  1. 定義功能測試
  2. 在 API 伺服器中實作端點

API的功能測試比Web部分的功能測試簡單。您可以在這裡找到測試程式碼。作為範例,讓我們看一下刪除使用者和對應端點的測試。

以下是刪除使用者的測試(delete_user.py):

from hamcrest import assert_that, equal_to
from requests import request, codes, Response

from src.storage.UserInfoType import UserInfoType
from tests.constants import BASE_URL, USR_URL, USR_INFO_URL
from tests.functional.utils.user import add_user


class TestDeleteUser:

    @staticmethod
    def _deleting(user_name: str) -> Response:
        url = f"{BASE_URL}/{USR_URL}/{user_name}"
        return request("DELETE", url)

    def test_delete_user(self, user_info: UserInfoType):
        add_user(user_info)

        response = self._deleting(user_info.name)

        assert_that(
            response.status_code,
            equal_to(codes.ok),
            "Invalid response status code",
        )

        url = f"{BASE_URL}/{USR_INFO_URL}/{user_info.name}"
        response = request("GET", url)
        assert_that(
            response.status_code,
            equal_to(codes.not_found),
            "User is not deleted",
        )

    def test_delete_nonexistent_user(self, user_info: UserInfoType):
        response = self._deleting(user_info.name)

        assert_that(
            response.status_code,
            equal_to(codes.not_found),
            "Invalid response status code",
        )

    def test_get_info_deleted_user(self, user_info: UserInfoType):
        add_user(user_info)

        self._deleting(user_info.name)

        url = f"{BASE_URL}/{USR_INFO_URL}/{user_info.name}"
        response = request("GET", url)
        assert_that(
            response.status_code,
            equal_to(codes.not_found),
            "User is not deleted",
        )

登入後複製

API 伺服器實作

Falcon 中的端點定義 (app.py)

import falcon.asgi

from src.resources.UserInfo import UserInfo
from src.resources.UserOperations import UserOperations
from .resources.Health import Health
from .storage.UsersInfoStorage import UsersInfoStorage
from .storage.UsersInfoStorageInMemory import UsersInfoStorageInMemory


def create_app(storage: UsersInfoStorage = UsersInfoStorageInMemory()):
    app = falcon.asgi.App(cors_enable=True)

    usr_ops = UserOperations(storage)
    usr_info = UserInfo(storage)

    app.add_route("/user", usr_ops)
    app.add_route("/user_info/{name}", usr_info)
    app.add_route("/user/{name}", usr_ops)
    app.add_route("/health", Health())

    return app

登入後複製

現在是時候對端點進行存根了。這允許我們運行 API 伺服器,儘管所有測試最初都會失敗。對於我們的存根,我們將使用返回狀態代碼為 501(未實現)的回應的代碼。

這是我們的 Falcon 應用程式的資源檔案之一中的存根範例:

class UserOperations:
    def __init__(self, storage: UsersInfoStorage):
        self._storage: UsersInfoStorage = storage

    async def on_get(self, req: Request, resp: Response):
        resp.status = HTTP_501        

    async def on_post(self, req: Request, resp: Response):
        resp.status = HTTP_501

    async def on_delete(self, _req: Request, resp: Response, name):
        resp.status = HTTP_501

登入後複製

讓我們用所需的程式碼取代每個存根,如 Web 元件中所示,直到所有測試都通過。 (端點的最終程式碼可以在這裡找到。)

這個過程稱為紅-綠-重構:

Functional Testing as a Design Tool for Full Stack Projects. Part II: API server and Project Release

以下是在端點中用真實程式碼取代存根以刪除使用者的範例:

class UserOperations:
    def __init__(self, storage: UsersInfoStorage):
        self._storage: UsersInfoStorage = storage

    async def on_get(self, req: Request, resp: Response):
        resp.status = HTTP_501        

    async def on_post(self, req: Request, resp: Response):
        resp.status = HTTP_501

    async def on_delete(self, _req: Request, resp: Response, name):
        try:
            self._storage.delete(name)
            resp.status = HTTP_200
        except ValueError as e:
            update_error_response(e, HTTP_404, resp)

登入後複製

應該要增加一個E2E測試來驗證新增使用者驗證刪除使用者的完整過程(e2e.py):

from hamcrest import assert_that, equal_to
from requests import request, codes

from src.storage.UserInfoType import UserInfoType
from tests.constants import BASE_URL, USR_URL, USR_INFO_URL
from tests.functional.utils.user import add_user
from tests.utils.auth import create_auth_headers


class TestE2E:
    def test_e2e(self, user_info: UserInfoType):
        add_user(user_info)

        url = f"{BASE_URL}/{USR_URL}"
        response = request("GET", url, headers=create_auth_headers(user_info))
        assert_that(response.status_code, equal_to(codes.ok), "User is not authorized")

        url = f"{BASE_URL}/{USR_INFO_URL}/{user_info.name}"
        response = request("GET", url)
        assert_that(
            response.json(),
            equal_to(dict(user_info)),
            "Invalid user info",
        )

        url = f"{BASE_URL}/{USR_URL}/{user_info.name}"
        request("DELETE", url)

        url = f"{BASE_URL}/{USR_INFO_URL}/{user_info.name}"
        response = request("GET", url)

        assert_that(
            response.status_code,
            equal_to(codes.not_found),
            "User should not be found",
        )

登入後複製

API 部分開發流程總結

整體而言,該過程與 Web 元件的過程相似,但更加簡化。這是因為我們在 Web 開發階段就已經定義了通用目的。我們這裡的重點主要是定義 API 的功能測試。

專案的 Web 和 API 部分現已完成並經過獨立測試,我們進入最後階段。

Functional Testing as a Design Tool for Full Stack Projects. Part II: API server and Project Release

Release of the Whole Project

The last step is to integrate the Web and API components. We’ll use End-to-End (E2E) testing in the Web part to facilitate this integration. As we defined earlier, the main purpose of the project is to enable user registration and sign-in. Therefore, our E2E test should verify the entire process of user registration and subsequent sign-in in one comprehensive test sequence.

It’s worth noting that E2E tests don’t use mocks. Instead, they interact directly with the Web app connected to the API server, simulating real-world usage. (e2e.spec.ts)

import {expect, test} from "@playwright/test";
import axios from 'axios';
import {fail} from 'assert'
import {faker} from "@faker-js/faker";
import {buildUserInfo, UserInfo} from "./helpers/user_info";
import {RegistrationPage} from "../infra/page-objects/RegisterationPage";
import {RegistrationSucceededPage} from "../infra/page-objects/RegistrationSucceededPage";
import {LoginPage} from "../infra/page-objects/LoginPage";
import {WelcomePage} from "../infra/page-objects/WelcomePage";


const apiUrl = process.env.API_URL;
const apiUserUrl = `${apiUrl}/user`

async function createUser(): Promise<UserInfo> {
    const userInfo = {
        name: faker.internet.userName(),
        password: faker.internet.password(),
        last_name: faker.person.lastName(),
        first_name: faker.person.firstName(),
    }
    try {
        const response = await axios.post(apiUserUrl, userInfo)
        expect(response.status, "Invalid status of creating user").toBe(axios.HttpStatusCode.Created)
    } catch (e) {
        fail(`Error while creating user info: ${e}`)
    }
    return userInfo
}

test.describe('E2E', {tag: '@e2e'}, () => {
    let userInfo = null
    test.describe.configure({mode: 'serial'});

    test.beforeAll(() => {
        expect(apiUrl, 'The API address is invalid').toBeDefined()
        userInfo = buildUserInfo()
    })

    test.beforeEach(async ({baseURL}) => {
        try {
            const response = await axios.get(`${apiUrl}/health`)
            expect(response.status, 'Incorrect health status of the API service').toBe(axios.HttpStatusCode.Ok)
        } catch (error) {
            fail('API service is unreachable')
        }
        try {
            const response = await axios.get(`${baseURL}/health`)
            expect(response.status, 'The Web App service is not reachable').toBe(axios.HttpStatusCode.Ok)
        } catch (error) {
            fail('Web App service is unreachable')
        }
    })

    test("user should pass registration", async ({page}) => {
        const registerPage = await new RegistrationPage(page).open()

        await registerPage.registerUser(userInfo)

        const successPage = new RegistrationSucceededPage(page)
        expect(await successPage.isOpen(), `The page ${successPage.name} is not open`).toBeTruthy()
    })

    test("user should login", async ({page}) => {
        const loginPage = await new LoginPage(page).open()

        await loginPage.login({username: userInfo.name, password: userInfo.password})

        const welcomePage = new WelcomePage(userInfo.name, page)
        expect(await welcomePage.isOpen(), `User is not on the ${welcomePage.name}`).toBeTruthy()
    })
});

登入後複製

As you can see, this is a sequence of tests. All the previously described tests in the Web and API parts are independent and can be run in parallel.

The Web and API components can be run separately as independent services via Docker containers.

Here’s the Dockerfile for the API server:

FROM python:3.11-alpine
ENV POETRY_VERSION=1.8.1
ENV PORT=8000
WORKDIR /app
COPY . .

RUN apk --no-cache add curl && pip install "poetry==$POETRY_VERSION" && poetry install --no-root --only=dev

EXPOSE $PORT

CMD ["sh", "-c", "poetry run uvicorn src.asgi:app --log-level trace --host 0.0.0.0 --port $PORT"]
登入後複製

Here’s the Dockerfile of the Web app:

FROM node:22.7.0-alpine
WORKDIR /app
COPY . .
ENV API_URL="http://localhost:8000"
ENV WEB_APP_PORT="3000"


RUN apk --no-cache add curl && npm install --production && npm run build

EXPOSE $WEB_APP_PORT

CMD ["npm", "start"]
登入後複製

The Docker composition of the both parts:

services:
  web:
    image: web
    container_name: web-app
    ports:
      - "3000:3000"
    environment:
      - API_URL=http://api:8000
    depends_on:
      api:
        condition: service_healthy
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:3000/health" ]
      interval: 5s
      timeout: 5s
      retries: 3

  api:
    image: api
    container_name: api-service
    ports:
      - "8000:8000"
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ]
      interval: 5s
      timeout: 5s
      retries: 3

networks:
  default:
    name: my-network

登入後複製

There is a script for a more convenient way of running E2E tests with the services.

It's worth noting that the ability to run the entire project or its parts separately locally demonstrates the project's testability and ease of development.

Testing should be an integral part of the CI/CD process. Therefore, workflows for CI/CD have been added to the project's repository on GitHub.

The following workflows run for each commit to the repository:

  • API - build and testing
  • Web - build, run as a service, and testing
  • E2E - run both services and perform E2E testing

Conclusion

These two parts have demonstrated how to design a Full Stack project through functional test definition. Both the Web and API parts have been designed and developed independently.

This approach allows for progression from general purpose definition to more detailed specifications without losing control of the project's quality and integrity.

While this project serves as a simple example, real-world projects are often more complex. Therefore, this method is particularly useful for designing separate features.

As mentioned earlier, one of the drawbacks of this approach is the time required for development.

Another drawback is the disconnection between project parts during development. This means there's no automatic synchronization of changes between parts. For example, if there's a code change in an endpoint definition in the API part, the Web part won't be automatically aware of it.

This issue can be addressed through human inter-team synchronization (in cases of small teams or low code change frequency) or by implementing Design by Contract methodology.

以上是功能測試作為全端專案的設計工具。第二部分:API伺服器和專案發布的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!