Раздел 21 · Haskell

Тестирование сервера

senior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Тестирование сервера

Сервис собран. Проверим его на трёх уровнях. Чистый домен ловим property-based тестами, как в уроке 30. Эндпоинты гоняем через hspec-wai прямо по WAI-приложению, без сети. И тот же тип API, что описывает сервер, даёт нам типобезопасный клиент для интеграционных проверок. А база под всем этим временная и в памяти, чтобы тесты были быстрыми и не зависели от внешнего Postgres.

Временная база: один трюк на весь тест

Прод ходит в Postgres, но тестам незачем поднимать целую базу. Запросы из урока 44 написаны над общим SqlBackend и не знают, какой драйвер под ними. Значит, в тестах можно подсунуть SQLite в памяти, и те же запросы будут работать.

mkTestApp :: IO Application
mkTestApp = do
  pool <- runNoLoggingT (createSqlitePool ":memory:" 1)
  runSqlPool runMigrations pool
  mkApp (Env pool testConfig newStderrLogger)

Пул из одного соединения держит базу живой ровно на время теста, а runMigrations создаёт таблицы. После теста соединение закрывается, и база исчезает, чистить нечего. Это и есть приём backend-agnostic запросов из урока 44, окупившийся в тестах.

hspec-wai: запросы без сети

hspec-wai прогоняет запросы прямо по Application, минуя сеть. with поднимает приложение один раз на блок, дальше идут привычные get/request с матчерами.

spec :: Spec
spec = with mkTestApp $ do
  describe "открытые эндпоинты" $ do
    it "GET /health отвечает 200" $
      get "/health" `shouldRespondWith` 200
    it "GET /games поначалу пустой список" $
      get "/games" `shouldRespondWith` "[]" {matchStatus = 200}

shouldRespondWith проверяет и код, и при желании тело. Это быстрые тесты: ни порта, ни сокета, всё в памяти процесса.

Сквозной сценарий авторизации

Самое ценное это проверить не один эндпоинт, а сценарий. Вот вся цепочка авторизации из урока 46: без токена нельзя, а после входа можно.

describe "защита эндпоинтов" $ do
  it "создание игры без токена это 401" $
    postJSON "/games" (encode catan) `shouldRespondWith` 401

  it "админ входит и создаёт игру" $ do
    token <- login adminEmail adminPassword
    postAuth "/games" token (encode catan) `shouldRespondWith` 200

login делает реальный POST /login, достаёт токен из тела ответа и возвращает его, а postAuth подставляет его в заголовок Authorization. Помощники тонкие:

login :: Text -> Text -> WaiSession st Text
login email password = do
  resp <- request methodPost "/login" [jsonCT] (encode (LoginRequest email password))
  case decode (simpleBody resp) of
    Just (LoginResponse token) -> pure token
    Nothing -> error "вход не удался"

postAuth :: ByteString -> Text -> BL.ByteString -> WaiSession st SResponse
postAuth path token =
  request methodPost path [jsonCT, (hAuthorization, "Bearer " <> encodeUtf8 token)]

Тест проходит весь путь: вход подписывает JWT, эндпоинт проверяет подпись, роль допускает действие. Если сломается любое звено, тест покраснеет.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.