قالب وردپرس درنا توس
Home / IOS Development / Steam testing | raywenderlich.com

Steam testing | raywenderlich.com



Testing is an important part of the software development process. Typewriter tests and their automation as much as possible let you develop and develop your applications quickly.

In this server-side guide, learn how to write tests for Steam applications. You learn why testing is important, how it works with Swift Package Manager (SPM), and how to write tests for your application.

Why should you write test?

Software testing is as old as software development itself. Modern server programs are distributed many times one day so it's important that you are sure that everything works as expected. Writing tests for your application gives you confidence that the code is sound.

Testing also gives you confidence when you refactor your code. Testing each part of your application manually is slow and laborious, even when your application is small! To quickly develop new features, you will ensure that the existing features do not break. Having an expansive set of tests allows you to verify everything that still works when you change your code.

Testing can also help you design your code. Test driven development is a popular development process where you write tests before writing code. This helps you get the full test coverage of your code. Test driven development also helps you design code and APIs.

Note : This tutorial assumes that you have experience using Damp to build web apps. See Getting Started with Server Page Swift with Steam if you are new to Steam. This tutorial also assumes that you have experience working with the command line, Floating and Docker.

For information on using liquid in steam, see Using liquid and renewable models in steam.

If you're new to Docker, check out Docker on macOS: Get started.

Getting Started

Download the start project for this tutorial using the Download Materials button at the top or bottom of this tutorial.

The startup project includes a pre-built Vapor app called TIL (Today I Learned) that hosts user-generated acronyms.

When testing on macOS, Xcode connects to a specific test target. Xcode configures a form to use that goal and you run your tests from Xcode. The objective C drive time scans XCTestCase s and picks out the methods whose name begins with test . You need a different approach when testing server-side Swift on Linux, since there is no Objective-C runtime. There is also no Xcode project to remember schemes and which tests belong to.

To get started on macOS, open Package.swift in the project. There is a test target defined in the targets array:

  .testTarget (name: "AppTests", dependencies: ["App"]),

This defines a [Target] Target type with a dependence on App . Tests must live in the catalog Tests / . In this case, it is Tests / AppTests .

From a Terminal in the project rotation, you generate an Xcode project and open it with steam xcode -y . If you select the TILApp package scheme, it is set up with AppTests as a test target. You can run these tests as normal with Command-U or Product ▸ Test :

Testing Users

Write your first test

Close your project in Xcode, then in Terminal, create a file for user-related tests:

  touch Test / AppTests / UserTests.swift
steam xcode -y

This places the file in the correct location in the directory hierarchy and regenerates the Xcode project to ensure that the new file builds correctly. In Xcode, open UserTests.swift and add the following:

  @testable import App
import steam
import XCTest
import FluentPostgreSQL

last class UserTests: XCTestCase {

}

This creates XCTestCase you want to use to test your users and import the required modules to make everything work.

Next, add the following in UserTests to try to get the users from the API:

  func testUsersCanBeRetrievedFromAPI () throws {
// 1
la expectedName = "Alice"
la expectedUsername = "alice"

// 2
var config = Config.default ()
var services = Services.default ()
var env = Environment.testing
try App.configure (& config, & env, & services)
let app = try application (config: config, environment: env, services: services)
try App.boot (app)

// 3
la conn = try app.newConnection (for: .psql) .wait ()

// 4
let user = user (name: expected name, username: expected username)
let savedUser = try user.save (at: conn) .wait ()
_ = try using (name: "luke", username: "lukes"). save (at: conn) .wait ()

// 5
la responder = try app.make (responder.self)

// 6
la request = HTTPRequest (method: .GET, url: URL (string: "/ api / users")!)
la wrappedRequest = Request (request: using: app)

// 7
la answer = try responder.respond (to: wrappedRequest) .wait ()

// 8
leave data = response.http.body.data
let users = try JSONDecoder (). decode ([User]. self, from: data!)

// 9
XCTAssertEqual (users.count, 2)
XCTAssertEqual (users [0] .name, expected name)
XCTAssertEqual (users [0] .username, expected username)
XCTAssertEqual (users [0] .id, savedUser.id)

// 10
conn.close ()
}

Much happens in this test; Here is the breakdown:

  1. Define some expected values ​​for the test: The user's name and username.
  2. Apply a Application similar to App.swift . This creates a whole Application object, but does not start running the program. This helps you ensure that you properly configure your program when your test calls the same App.configure (_: _: _ :) . Note that you are using the environment here.
  3. Create a database connection to perform database operations. Notice the use of .wait () here and throughout the test. When you do not run the test on a EventLoop you can use wait () to wait for the future to return. This helps simplify the code.
  4. Create a few users and save them to the database.
  5. Create a Responder type; This is what answers your requests.
  6. Send a GET HTTPRequest to / api / users endpoint to get all the users. A Inquiry object wraps HTTPRequest so it is a Worker to execute it. Since this is a test, you can force unpack variables to simplify the code.
  7. Send the request and get the answer.
  8. Decodes the response data in a series of User s.
  9. Make sure there are the correct number of users in the answer, and users match those created at the start of the test.
  10. Close the connection to the database when the test is complete and stop the application to properly release used resources.

Then you need to update the app's configuration to support testing. Open configure.swift and under were databases = DatabasesConfig () add the following:

  leave database name: String
leave database port: Int
// 1
if (env == .testing) {
databaseName = "steam test"
databasePort = 5433
} other {
databaseName = "damp"
databasePort = 5432
}

This sets the properties of the database name and port depending on the environment. You use different names and ports to test and run the program. Then the call replaces PostgreSQLDatabaseConfig with the following:

  la databaseConfig = PostgreSQLDatabaseConfig (
host name: "localhost",
port: database port,
username: "steam",
database: databaseName,
password: "password")

This sets the database port and the name from the properties specified above. These changes allow you to run your tests on a different database than your production database. This ensures that you start each test in a known state and do not destroy live data.

The VaporTIL app was developed using Docker to host the app database. Setting up another database on the same machine for testing is straightforward. In the terminal, enter the following:

  docker run - name postgres-test -e POSTGRES_DB = steam test
-e POSTGRES_USER = vapor -e POSTGRES_PASSWORD = password
-p 5433: 5432 -d postgres

This changes the container name and the database name. The dock container has also been mapped to host port 5433 to avoid conflict with the existing database.

Run the tests and they will pass. But if you run the tests again, they will fail. The first test drive made two users to the database, and the second test run now has four users since the database was not reset.

Open configure.swift and add the following at the bottom of ] configure (_: _: _ :) :

  var commandConfig = CommandConfig.default ()
commandConfig.useFluentCommands ()
services.register (commandConfig)

This adds floating commands to your application that allow you to manually run migrations. It also lets you reset your moves. Open UserTests.swift and at the beginning of testUsersCanBeRetrievedFromAPI () add the following:

  // 1
la revertEnvironmentArgs = ["vapor", "revert", "--all", "-y"]
// 2
was revertConfig = Config.default ()
was revertServices = Services.default ()
was revertEnv = Environment.testing
// 3
revertEnv.arguments = revertEnvironmentArgs
// 4
try App.configure (& revertConfig, & revertEnv, & revertServices)
la revertApp = try Application (config: revertConfig, environment: revertEnv,
services: revertServices)
try App.boot (revertApp)
// 5
try revertApp.asyncRun (). wait ()

// 6
la migrateEnvironmentArgs = ["vapor", "migrate", "-y"]
var migrateConfig = Config.default ()
var migrateServices = Services.default ()
var migrateEnv = Environment.testing
migrateEnv.arguments = migrateEnvironmentArgs
try App.configure (& migrateConfig, & migrateEnv, & migrateServices)
let migrateApp = try Application (config: migrateConfig, environment: migrateEnv,
services: migrate services)
try App.boot (migrateApp)
try migrateApp.asyncRun (). wait ()

Here's what this does:

  1. Enter the arguments that Application will perform.
  2. Set up the services, configuration, and test environment.
  3. Enter the arguments in the environment.
  4. Set up the application as before in the test. This creates another Application object that executes the return command.
  5. Call asyncRun () to launch the program and return the command.
  6. Repeat the process again to run the transfers. This sets up the database on a separate connection, such as how Damp does.

Build and run the tests again, and this time they will pass!

Test extensions

The first test contains a lot of code that all tests need. You can extract the common parts to make testing easier to read and simplify future testing.

Close your project in Xcode and then create two new files for this extension in Terminal:

  touch Test / AppTests / Application + Testable.swift
touch Tests / AppTests / Models + Testable.swift
steam xcode -y

When the project has regenerated, open Application + Testable.swift and add the following:

  Import Steam
import app
import FluentPostgreSQL

extension Application {
static func testable (envArgs: [String]? = zero) cast -> Application {
var config = Config.default ()
var services = Services.default ()
var env = Environment.testing

if let the environmentArgs = envArgs {
env.arguments = environmentArgs
}

try App.configure (& config, & env, & services)
let app = try application (config: config, environment: env, services: services)

try App.boot (app)
return the app
}
}

This feature allows you to create a testable Application object. You can set environmental arguments, if necessary. This removes multiple lines of duplicate code in your test.

Under testable (envArgs :) add the following function to reset the database:

  static func reset () cast {
la revertEnvironment = ["vapor", "revert", "--all", "-y"]
try Application.testable (envArgs: revertEnvironment) .asyncRun (). wait ()
la migrateEnvironment = ["vapor", "migrate", "-y"]
try Application.testable (envArgs: migrateEnvironment) .asyncRun (). wait ()
}

This uses the above function to create a program that executes the command command, and then executes the Migrate command. This simplifies the recovery of the database in each test.

Then add the following at the bottom of the file:

  struct EmptyContent: Content {}

This defines an empty Content type to be used when there is no body to submit a request. Since you cannot define nil for a generic type, EmptyContent can give you a type that satisfies the compiler.

Now, under reset () ] add the following:

  // 1
func sendRequest  (to path: String, method: HTTPMethod, headers: HTTPHeaders = .init (),
body: T? = zero) throws -> Answer where T: Content {
la responder = try self.make (responder.self)
// 2
la request = HTTPRequest (method: method, URL: URL (strict: path)!)
headings: headlines)
la wrappedRequest = Request (http: request, use: self)
// 3
if let body = body {
try wrappedRequest.content.encode (body)
}
// 4
return sample responder.respond (to: wrappedRequest) .wait ()
}

// 5
func sendRequest (to path: String, method: HTTPMethod,
headers: HTTPHeaders = .init ()) cast -> Reply {
// 6
la emptyContent: EmptyContent? = zero
// 7
return sample sendRequest (to: path, method: method, headings: headings,
body: empty content)
}

// 8
func sendRequest  (to path: String, method: HTTPMethod, headers: HTTPHeaders,
data: T) throws where T: content {
// 9
_ = try self.sendRequest (for: path, method: method, headings: headings,
body: data)
}

Here's what the code does:

  1. Define a method that sends a request to a path and returns a Answer . Allow the HTTP method and headers to be specified; This applies to later tests. Also allow an optional, generic Content to be given to the body.
  2. Make a responder, request and wrapped request as before.
  3. If the test contains a body, you encode the body into the content of the requests . By using Vapor's encoder (_ :) you can take advantage of all the custom encoders you have inserted.
  4. Send the request and send the reply.
  5. Define a convenience method that sends a request to a body without body.
  6. Create a EmptyContent to satisfy the compiler of a body parameter.
  7. Use the method created earlier to send the request.
  8. Define a method that sends a request to a path and accepts a generic Content type. This convenience method lets you send a request when you don't care about the answer.
  9. Use the first method created above to send the request and ignore the answer.

Under these helpers, add the following methods to get a response from a request:

  // 1
func getResponse  (to path: String, method: HTTPMethod = .GET,
headings: HTTPHeaders = .init (), data: C? = zero,
decodeTo type: T.Type) cast -> T where C: Content, T: Decodable {
// 2
la response = try self.sendRequest (to: path, method: method,
headings: headlines, body: data)
// 3
return sample response.content.decode (type) .wait ()
}

// 4
func getResponse  (to path: String, method: HTTPMethod = .GET,
headings: HTTPHeaders = .init (),
decodeTo type: T.Type) cast -> T where T: Decodable {
// 5
la emptyContent: EmptyContent? = zero
// 6
return test self.getResponse (to: path, method: method, headings: headings,
data: emptyContent, decodeTo: type)
}

Here is what happens:

  1. Define a generic method that accepts a type Content type and Decodable to get an answer to a request.
  2. Use the steps created above to send the request.
  3. Decode the response body to the generic type and return the result.
  4. Define a generic convenience method that accepts a Decodable type to answer a request without giving a body.
  5. Create a blank Content to satisfy the compiler.
  6. Use the previous method to get the answer to the request.

Next, open Models + Testable.swift and create an extension to create a user :

  @ testable import tab
import FluentPostgreSQL

extension user {
static func create (name: String = "hatch", username: String = "close",
on connection: PostgreSQLConnection) throws -> User {
let user = user (name: name, username: username)
return sample user.save (on: connection) .wait ()
}
}

This feature stores a user created with the included details in the database. It has default values, so you don't have to give anyone if you don't care about them.

With all this created, you can write about your user test now. Open UserTests.swift and delete testUsersCanBeRetrievedFromAPI () .

UserTests creates the common properties of all the tests:

  let usersName = "Alice"
leave usersUsername = "alicea"
leave usersURI = "/ api / users /"
was app: Application!
was conn: PostgreSQLConnection!

Then use setUp () to run the code that must be performed before each test:

  override func setUp () {
try! Application.reset ()
app = try! Application.testable ()
conn = try! app.newConnection (to: .psql) .wait ()
}

This reflects the database, generates a Application for the test, and establishes a connection to the database.

Now implement teardown () to close the connection to the database and shut down the program:

  override func tearDown () {
conn.close ()
try? app.syncShutdownGracefully ()
}

Change rewrite testUsersCanBeRetrievedFromAPI () to use all new help methods:

  func testUsersCanBeRetrievedFromAPI () cast {
let user = try User.create (name: usersName, username: usersUsername,
at: conn)
_ = try User.create (at: conn)

let users = try the app.getResponse (to: usersURI, decodeTo: [User] .self)

XCTAssertEqual (users.count, 2)
XCTAssertEqual (users [0] .name, usersName)
XCTAssertEqual (users [0] .username username username)
XCTAssertEqual (users [0] .id, user.id)
}

This test does exactly the same as before, but is far more readable. It also makes the next tests much easier to write. Run the tests again to make sure they still work.

Testing User API

In UserTests.swift use test help methods to test storage of a user via the API by adding the following test method:

  func testUserCanBeSavedWithAPI () throws {
// 1
let user = user (name: username, username: users username)
// 2
let received User = try the app.getResponse (
to: userURI,
method: .POST,
headings: ["Content-Type": "application/json"],
data: user,
decode to: User.self)

// 3
XCTAssertEqual (received username, username)
XCTAssertEqual (received username, username, username)
XCTAssertNotNil (receivedUser.id)

// 4
let users = try the app.getResponse (to: usersURI, decodeTo: [User] .self)

// 5
XCTAssertEqual (users.count, 1)
XCTAssertEqual (users [0] .name, usersName)
XCTAssertEqual (users [0] .username username username)
XCTAssertEqual (users [0] .id, receivedUser.id)
}

Here's what the test does:

  1. Create a User object with known values.
  2. Use getResponse (to: method: headers: data: decodeTo :) to send a POST request to the API and get the answer. Use the user's object as the request body and set the headers correctly to simulate a JSON request. Convert the answer to a User object.
  3. Specify that the response from the API matches the expected values.
  4. Get all the users from the API.
  5. Make sure the answer only contains the user you created in the first request.

Run the tests to make sure the new test works!

Then, add the following test to retrieve a single user from the API:

  func testGettingASingleUserFromTheAPI () cast {
// 1
let user = try User.create (name: usersName, username: usersUsername,
at: conn)
// 2
let receivedUserUser = try the app.getResponse (to: "(usersURI) (user.id!)",
decode to: User.self)

// 3
XCTAssertEqual (received username, username)
XCTAssertEqual (received username, username, username)
XCTAssertEqual (receivedUser.id, user.id)
}

Here's what the test does:

  1. Save a user in the database with known values.
  2. Get the user at / api / users / < USER ID> .
  3. Specifying the values ​​are the same as specified when creating the user.

The final part of the user's API to test retrieves the user's acronyms. Open Models + Testable.swift and at the end of the file, create a new extension to create acronyms:

  extension Acronym {
static func create (short: String = "ON",
lang: String = "Today I learned",
User: User? = zero,
on connection: PostgreSQLConnection) throws -> Acronym {
were acronymsUser = user

if acronymsUser == nil {
akronymerUser = try User.create (on: connection)
}

la acronym = Acronym (short: short, long: long, userID: acronymsUser! .id!)
return sample acronym.save (on: connection) .wait ()
}
}

This creates an acronym and stores it in the database with the specified values. If you do not enter any values, it uses default settings.

Open UserTests.swift and create a method for testing the user's acronyms:

  func testGettingAUsersAcronymsFromTheAPI () throws {
// 1
let user = try User.create (on: conn)
// 2
la acronymShort = "OMG"
la acronymLong = "Oh my God"
// 3
la acronym1 = try Acronym.create (short: acronymShort, long: acronymLong,
user: user, on: conn)
_ = try Acronym.create (short: "LOL", long: "Laugh Out Loud", user: user,
at: conn)

// 4
la acronyms = try the app.getResponse (to: "(usersURI) (user.id!) / acronyms",
decode to: [Acronym] .self)

// 5
XCTAssertEqual (acronym.count, 2)
XCTAssertEqual (acronyms [0] .id, acronym1.id)
XCTAssertEqual (acronyms [0] .card, acronymShort)
XCTAssertEqual (acronyms [0] .long, acronymLong)
}

Here's what the test does:

  1. Create a user for acronyms.
  2. Define some expected values ​​for an acronym.
  3. Create two acronyms in the database using the created user. Use the expected values ​​for the first acronym.
  4. Get the user's acronyms from the API by sending a request to / api / users / < USERID> / acronyms .
  5. Specify that the answer returns the correct number of acronyms, and the first corresponds to the expected values.

    Open

    Models + Testable.swift and at the bottom of the file, add a new extension to simplify creating acronym categories:

      extension App.Category {
    static func create (name: String = "Random",
    on connection: PostgreSQLConnection) throws -> App.Category {
    leave category = Category (name: name)
    return sample category.save (on: connection) .wait ()
    }
    }
    

    Like the other model help functions, (name: on :) name creates as a parameter and creates a category in the database. Tests for Acronyms API and Categories API is part of the start-up project for this tutorial. Open CategoryTests.swift and uncomment all the code. The tests follow the same pattern as user tests.

    Open AcronymTests.swift and uncomment all code. These tests also follow a similar pattern before, but there are some additional tests for the extra routes in the acronym API. These include updating an acronym, deleting an acronym, and the various floating query routes.

    Run all tests to make sure they all work.

    Where to go from here?

    You can download the completed project for this tutorial using Download the materials button at the top or bottom of this tutorial.

    In this guide, you learned how to test your steam applications to make sure they work properly. Server side Swift apps are usually distributed to Linux, and writing tests for your application means you can run these tests on Linux. This gives you confidence, the program will work when you distribute it. Having a good test package lets you develop and customize your applications quickly. To learn about testing steam applications on Linux, see Server-Side Swift: Testing on Linux.

    Dam's architecture has great confidence in protocols. This, combined with the steam dependency injection service frame, makes testing easy and scalable. For large applications, you may even want to introduce a data abstraction layer so that you do not test with a real database.

    This means that you do not need to connect to a database to test your main logic and will speed up the tests.

    It is important that you run tests regularly. Using a continuous integration system (CI) such as Jenkins or Bitbucket pipelines, you can test all your commitments. You also need to keep the tests up to date.

    Questions or comments on this tutorial? Leave them in the comments below!


Source link