Тестирование сервера
Сервис собран. Проверим его на трёх уровнях. Чистый домен ловим 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-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.