在 Vapor 3 中删除测试数据库

Deleting test database in Vapor 3

我想为 Vapor 3 服务器编写一些集成测试,并且每次 运行 我的测试都需要干净的 Postgre 数据库。我怎样才能做到这一点?似乎迁移不是正确的方法,因为如果数据库尚不存在,它们已经运行宁了一次。

看看https://github.com/raywenderlich/vapor-til/tree/master/Tests

这需要数据库在 运行 测试之前 运行 宁,但它会在每个测试开始时恢复所有迁移 运行,这为您提供了一个干净的数据库每一次。 (特别是here

根目录中还有一个 docker-compose.yml,用于在 Linux

上启动一个完全隔离的测试环境

我找到了一个资源密集度较低的解决方案,然后每次都恢复所有迁移。

RSpec 有一个配置 (use_transactional_fixtures) 允许将每个测试包装在一个 SQL 事务中。当测试结束时,它将回滚事务并因此还原测试期间发生的所有更改。相关 documentation is here.

我们可以在 Vapor 中实现类似的解决方案。我的示例测试如下所示。

final class VaporTests: XCTestCase {

  var app: Application!

  override func setUp() {
    super.setUp()

    app = try! Application.buildForTesting()
    let conn = try! app.requestPooledConnection(to: .psql).wait()
    try! conn.simpleQuery("BEGIN TRANSACTION").wait()
    try! app.releasePooledConnection(conn, to: .psql)
  }

  override func tearDown() {
    let conn = try! app.requestPooledConnection(to: .psql).wait()
    try! conn.simpleQuery("ROLLBACK").wait()
    try! app.releasePooledConnection(conn, to: .psql)

    super.tearDown()
  }

  func testExample() throws {
    let request = HTTPRequest(method: .GET, url: "my/endpoint/example")
    let wrapper = Request(http: request, using: app)

    let response = try ExampleController().example(wrapper).wait()

    XCTAssertEqual(response, .ok)
  }
}

为确保我不会遇到并发问题,我将测试应用程序中的数据库池限制为 1 个连接。

func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
  // ... other configurations

  let poolConfig = DatabaseConnectionPoolConfig(maxConnections: 1)
  services.register(poolConfig)
}

非常感谢 Jakub Jatczak 帮助我找出在 Rails 中发生这种情况的原因。

晚会已经很晚了,但是下面的方法也可以使 revertmigrate 命令起作用。此代码执行与@0xTim 给出的答案类似的命令。但是我使用了 Console.framework:

大多数情况下我们使用如下 configure.swift 文件:

import FluentPostgreSQL
import Vapor

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    // Register providers first
    try services.register(FluentPostgreSQLProvider())

    ...


    /// Configure commands
    var commandConfig = CommandConfig.default()
    commandConfig.useFluentCommands()
    services.register(commandConfig)

    ...

    /// Configure migrations
    services.register { container -> MigrationConfig in
        var migrationConfig = MigrationConfig()
        try migrate(migrations: &migrationConfig)
        return migrationConfig
    }
}

晚会已经很晚了,但下面的代码确实执行了恢复和迁移 命令:(我正在使用 QuickNimble 所以 beforeSuite。注释代码在那里,因为除非你使用上面的 configure.swift 你可以取消注释代码并使用 CommandConfig直接。)

import Quick
import Vapor
import Console
import FluentPostgreSQL
...
        configuration.beforeSuite {
            let console: Console = Terminal()
//            var commandConfig = CommandConfig()
//            commandConfig.use(RevertCommand(), as: "revert")
//            commandConfig.use(MigrateCommand(), as: "migrate")
            var config = Config.default()
            var env = Environment.testing
            var services = Services.default()

            do {
//                try App.configure(&config, &env, &services)
                let container = try Application(config: config, environment: env, services: services)
                let commandConfig = try container.make(CommandConfig.self)
                let commands = try commandConfig.resolve(for: container).group()
                var input = CommandInput(arguments: ["vapor","revert","--all", "-y"])
                try console.run(commands, input: &input, on: container).wait()
                input = CommandInput(arguments: ["vapor","migrate","-y"])
                try console.run(commands, input: &input, on: container).wait()
            } catch let error {
                console.error(error.localizedDescription)
                exit(1)
            }
        }

对于那些正在寻求另一种不涉及注册新迁移的方法(并且对我来说,增加更多代码复杂性)的人,您可以使用 Pre-Action 脚本来测试目标( ⌘ + < )

通过使用 bash 脚本,您可以创建一个全新的 postgresql 数据库,该数据库将用于构建仅供测试的项目:

# Variables 
export IS_TEST=true
export DB_USERNAME="`whoami`"
export DB_DBNAME="BARTENDER_TEST_DB"


#Creating dedicated Postgres DB 
echo "deleting & recreating $DB_DBNAME for user $DB_USERNAME"
psql postgres<< EOF
  DROP DATABASE "$DB_DBNAME";
  CREATE DATABASE "$DB_DBNAME";
  \list
EOF

然后在 configure.swift 文件中创建一个 PostgreSQLDatabaseConfig 匹配新创建的数据库

    if let _ = Environment.get("IS_TEST") { // IS_TEST is defined only in Pre-Action script 

        guard let username = Environment.get("DB_USERNAME") else {
            fatalError("Failed to create PostgresConfig - DB_USERNAME in Environment variables")
        }
        guard let databasename = Environment.get("DB_DBNAME") else {
            fatalError("Failed to create PostgresConfig - DB_DBNAME in Environment variables")
        }

        postgresqlConfig = PostgreSQLDatabaseConfig(
            hostname: "127.0.0.1",
            port: 5432,
            username: username,
            database: databasename,
            password: nil
        )
    } 
    else { /* your other config here */ }

    let database = PostgreSQLDatabase(config: postgresqlConfig)
    ... 

我在其中发现的最大优势是我什至可以从另一个项目触发 vapor buildvapor run,这将为我的持续集成测试创建一个全新的 Web 服务环境,只需插入正确的环境变量

我发现的最简单的方法是将 PostgresKit 添加到测试目标并使用它来设置连接以调用我的“清理”查询。

@testable import App
import Vapor
import XCTest
import PostgresKit

final class UserTests: XCTestCase {
    
    var pools: EventLoopGroupConnectionPool<PostgresConnectionSource>!
    var postgresDb: PostgresDatabase!
    var eventLoopGroup: EventLoopGroup!
    
    override func setUp() {
        let configuration = PostgresConfiguration(
            hostname: "localhost",
            username: "postgres",
            password: "password",
            database: "db_name"
        )
        
        eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)
        
        pools = EventLoopGroupConnectionPool(
            source: PostgresConnectionSource(configuration: configuration),
            on: eventLoopGroup
        )
        
        postgresDb = pools.database(logger: Logger.init(label: "TestLogger"))
        
    }
    
    override func tearDown() {
        let _ = try! postgresDb.query("DELETE FROM \(User.schema)").wait()
        try! pools.syncShutdownGracefully()
        try! eventLoopGroup.syncShutdownGracefully()
    }
    
    func testUploadUser() throws {
        let app = Application(.testing)
        defer { app.shutdown() }
        try configure(app)
        
        try app.testable(method: .running).test(.POST, "api/users", beforeRequest: { req in
            try req.content.encode(["firstName" : "Dwide", "lastName" : "Shrewd"])
        }, afterResponse: { res in
            XCTAssertEqual(res.status, .ok)
            let user = try res.content.decode(User.self)
            XCTAssertEqual(user, User(id: user.id, firstName: "Dwide", lastName: "Shrewd"))
        })
    }
    
}

这是我的 Package.swift

// swift-tools-version:5.2
import PackageDescription

let package = Package(
    name: "MyVaporProject",
    platforms: [
       .macOS(.v10_15)
    ],
    dependencies: [
        //  A server-side Swift web framework.
        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
        .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0")
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
                .product(name: "Vapor", package: "vapor")
            ],
            swiftSettings: [
                // Enable better optimizations when building in Release configuration. Despite the use of
                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                // builds. See <https://github.com/swift-server/guides#building-for-production> for details.
                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
            ]
        ),
        .target(name: "Run", dependencies: [.target(name: "App")]),
        .testTarget(
            name: "AppTests",
            dependencies: [
                .target(name: "App"),
                .product(name: "XCTVapor", package: "vapor"),
                .product(name: "PostgresKit", package: "postgres-kit")
            ]
        )
    ]
)

与之前的回答一样,这需要一个独立的 Postgres 数据库,已经迁移,准备好在测试开始前进行连接。