9

I want to write some integration tests for Vapor 3 server and I need to have clean Postgre database each time I run my tests. How can I achieve this? It seems migrations isn't the right way to go as they've been running once if database doesn't exist yet.

m8labs
  • 3,671
  • 2
  • 30
  • 32
  • Have you tried using raw SQL queries ? Also, `drop table` is part of the SQL package – nathan Nov 20 '18 at 14:33
  • @nathan and what is the proper place to run this query in vapor3 project? – m8labs Nov 20 '18 at 16:42
  • 1
    Using XCTestCase ? https://stackoverflow.com/questions/29822457/how-to-run-one-time-setup-code-before-executing-any-xctest – nathan Nov 20 '18 at 16:50

5 Answers5

5

Have a look at https://github.com/raywenderlich/vapor-til/tree/master/Tests

This requires a DB to be running before you run the tests, but it reverts all migrations at the start of each test run, which gives you a clean DB each time. (Specifically here)

There's also a docker-compose.yml in the root directory for spinning up a completely isolated test environment on Linux

0xTim
  • 5,146
  • 11
  • 23
  • I've inspected sources and have some concerns - isn't it extremely wasteful to run different instance of the Application (2 times in a row) before _each_ test method? Why don't they use "override class func setUp()" instead?... – m8labs Nov 22 '18 at 15:11
  • Running the commands (revert and prepare) require an application to boot up. When you provide the application a command it will exit upon completion, so if you do it the way above you don't have much choice for integration tests – 0xTim Nov 23 '18 at 13:01
  • I am using the method from the RW repo as well, and find that tests are very slow due to all the reverting re-migrating going on. Have 8 tests and it takes 8 seconds to run. An identical backend I wrote in Python/Django with identical tests takes only 0.3 seconds.. – Kevin Renskers Mar 30 '19 at 23:08
1

I've found a solution that is less resource-intensive, then reverting all migrations every time.

RSpec has a configuration (use_transactional_fixtures) that allows wrapping every test in an SQL transaction. When testing is over it will rollback the transaction and in consequence revert all the changes that happened during testing. Relevant documentation is here.

We can implement a similar solution in Vapor. My example test looks like this.

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)
  }
}

To make sure that I don't encounter issues with concurrency I'm limiting database pool to 1 connection in the test application.

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

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

Many thanks to Jakub Jatczak for helping me to find out how this happens in Rails.

Tomasz Bąk
  • 6,124
  • 3
  • 34
  • 48
  • Strange, when I try this, the tests never run, they simply hang forever on migrating the database. – Kevin Renskers Mar 30 '19 at 23:12
  • When I saw tests hanging, that usually meant, that I blocked the only accessible connection without releasing it. Make sure that your code around tests doesn't cause deadlock. – Tomasz Bąk Apr 01 '19 at 09:12
1

Quite late to the party but following way also does the revert and migrate command work. This code does the similar commands as answer given by @0xTim. But I have made use of Console.framework:

Mostly we use a configure.swift file like below:

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
    }
}

Quite late to the party but following code does execute revert and migrate commands: (I am using Quick and Nimble so beforeSuite. Commented code is there because unless you use above configure.swift you can just uncomment the code and make use of CommandConfig directly.)

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)
            }
        }
ahsumra
  • 87
  • 10
0

For thoses who are seeking another approach that doesnt involve registering new migrations ( and, to me, adding more code complexity ) you can use a Pre-Action script for tests target ( ⌘ + < )

Pre-Action Script

By using a bash script you can create a brand new postgresql database that will be used to build the project for tests only :

# 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

Then in configure.swift file you create a PostgreSQLDatabaseConfig that matches the newly created database

    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)
    ... 

The big advantage I found in this it that I can even trigger a vapor build and vapor run from another project that will create a brand new Webservice environment for my continuous integration testings, just by inserting the correct environment variables

Olympiloutre
  • 2,268
  • 3
  • 28
  • 38
0

Simplest way I found was to add PostgresKit to the test target and use it to setup a connection to call my "clean-up" queries.

@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"))
        })
    }
    
}

And this is my 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")
            ]
        )
    ]
)

As in previous answers, this requires a stood-up Postgres database, already migrated, ready to take connections before the tests start.

flopshot
  • 1,153
  • 1
  • 16
  • 23