Skip to content

Adds Testing #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Nov 24, 2021
Merged

Adds Testing #75

merged 43 commits into from
Nov 24, 2021

Conversation

joshuawright11
Copy link
Member

@joshuawright11 joshuawright11 commented Nov 24, 2021

The main goal of this PR is to bring test coverage to a respectable percent (8% -> 90%), and build in some mechanics to make it super simple to write tests for alchemy apps.

Sadly, async tests are still unavailable on Linux so I'll wait to merge this into main until then.

I also updated how configs work (hopefully for the last time) to something that can be nicely isolated in independent config files.

Testing

There's tons of affordance for testing & making assertions on services like Database, Cache, Queue, Client, etc. Most of the interfaces are heavily inspired by Laravel.

There's a new AlchemyTest target that adds a bunch of convenience methods for mocking various service. Using the TestCase<MyApp> target, you can easily test the various routes of your app.

import AlchemyTest

final class MyAppTests: TestCase<MyApp> {
    func testRoutes() {
        // Test unauthorized user access
        try await get("/user")
            .assertUnauthorized()
        
        // Test token authored user
        try await withBearerAuth("my_token").get("/user")
            .assertOk()
    }
}

Client

The request builder functions for TestCase made perfect sense as a convenience wrapper around HTTPClient, so there's a new Client class, with a default instance aliased to Http to make it easy to make HTTP requests.

try await Http.get("http://example.com")
try await Http.withJSON(["name": "Make amazing apps"]).post("https://api.todos.com/todo")

Configs

Previously, configs were all done in the boot function. It made things a messy and meant that lots of logic configuring separate services was in the same function. To keep configuration logic isolated on a per service basis, there's a new Configurable protocol to indicate a service for which your app has custom configurations. Services like Database, Cache, and Queue offer config types for your app to provide.

// Previously in MyApp.swift
struct MyApp: Application {
    func boot() {
        Database.configure(default: .postgres(host: "localhost", port: 5432, database: "alchemy"))
        Database.default.migrations = [
            CreateUsers(),
            CreateTodos(),
        ]

        Database.configure("mysql", config: .mysql(host: "localhost", port: 3306, database: "alchemy"))
        Redis.configure(default: .connection("localhost", port: 6379))
        // Other configs...
    }
}

// With `Configurable`, in Configs/Database.swift
extension Database: Configurable {
    
    /// Configurations related to your app's databases.
    
    public static var config = Config(
        
        /// Define your databases here
        
        databases: [
            .default: .postgres(
                host: Env.DB_HOST ?? "localhost",
                port: Env.DB_PORT ?? 5432,
                database: Env.DB ?? "alchemy",
                username: Env.DB_USER ?? "alchemy",
                password: Env.DB_PASSWORD ?? "",
                enableSSL: Env.DB_ENABLE_SSL ?? true
            ),
            "mysql": .mysql(
                host: Env.DB_HOST ?? "localhost",
                port: Env.DB_PORT ?? 5432,
                database: Env.DB ?? "alchemy",
                username: Env.DB_USER ?? "alchemy",
                password: Env.DB_PASSWORD ?? "",
                enableSSL: Env.DB_ENABLE_SSL ?? true
            ),
        ],
        
        /// Migrations for your app
        
        migrations: [
            Cache.AddCacheMigration(),
            Queue.AddJobsMigration(),
            CreateUsers(),
            CreateTodos(),
        ],
        
        /// Seeders for your databsase

        seeders: [
            DatabaseSeeder()
        ],
        
        /// Any redis connections can be defined here
        
        redis: [
            .default: .connection(Env.REDIS_HOST ?? "localhost")
        ]
    )
}

Note the new database Seeders.

In tests, you can easily sub a more test appropriate interface of a service (in memory cache / queue, in memory SQLite database, etc) into your app's service container using .fake() methods in AlchemyTest.

This means that mocking your database with an in memory SQLite instance is breeze.

import AlchemyTest

func testUser() {
    // Subs the default database out for a testable, in memory SQLite one.
    Database.fake()

    _ = try await withJSON(["email": "[email protected]", "password":"P@ssw0rd1"])
        .post("/user")

    AssertEqual(try await User.all().count, 1)
}

@joshuawright11 joshuawright11 merged commit 82ce125 into async Nov 24, 2021
@joshuawright11 joshuawright11 deleted the testing branch April 17, 2022 00:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant