Vapor testing can't find Swift OpenAPI Generator created endpoints

Hi everyone,

I'm playing around with the Swift OpenAPI Generator and Vapor to see if it can be an alternative to our TypeScript based tech stack.

So far I've created a simple toy project which uses an OpenAPI spec. I can get this one running and call the endpoints as per the expected specs. :white_check_mark:

Now I'm looking at testing the API. I followed the instructions on the Vapor forum: Vapor: Advanced โ†’ Testing

I have created the withApp method:

private func withApp(_ test: (Application) async throws -> ()) async throws {
    let app = try await Application.make(.testing)
    do {
        try await configure(app)
        try await test(app)
    }
    catch {
        try await app.asyncShutdown()
        throw error
    }
    try await app.asyncShutdown()
}

and a simple test:

        @Test("then it should return a status 200 (OK) message") func thenItShouldReturnAStatus20OKMessage() async throws {
            try await withApp { app in
                try await app.testing().test(.GET, "v1/rovers") { res async in
                    #expect(res.status == .ok)
                }
            }
        }

This test consistently fails with a 404 (not found) error. Though the logs shows indeed the /v1/rovers endpoint is called.

However, when I run the server and call the endpoint directly using curl http://localhost:8080/v1/rovers/ I do get the expected result back:

{
  "links" : [

  ],
  "rovers" : [
    {
      "id" : "BFE53D64-0DD9-4592-8936-BF52034D9C0F",
      "links" : [

      ],
      "pose" : {
        "heading" : "N",
        "x" : 1,
        "y" : 2
      }
    },
...

Am I missing something here? And/or am I supposed to test OpenAPI Generator based endpoints in a different way?

Where do you register the handlers? Is that done in your configure?

Hi Tim,

Yes, it's a very simple configure method:

// main.swift
let app = try await Vapor.Application.make()

func configure(_ app: Vapor.Application) async throws {
    let transport = VaporTransport(routesBuilder: app)
    let handler = MarsRoverAPIImplementation()
    
    try handler.registerHandlers(on: transport)
}

try await configure(app)
try await app.execute()

That looks like it should work fine - is there a project available to try and reproduce it?

Thank you Tim,

I'll try and make a stripped out version of the project tonight.

KR Maarten

OK, here's a sample project (hope this way of sharing works): iCloud Drive - Apple iCloud

Also, I notice now it no longer builds the tests (regular build is fine) and shows these errors (which may or may not be related):

I haven't checked the project but (regarding the errors in the image) off-hand I'd guess there is either a missing import or a missing dependency declaration in Package.swift. Perhaps "VaporTesting" or "Testing"?

The project builds for me, I suggest you clean the project or if that doesn't work, clear Derived Data and it should work fine

Did you also get the tests to pass?

I got the project to build again. Was indeed missing the VaporTesting dependency. But the tests still fail (unexpectedly)

Yes, though the example project you provided only has a single empty test

then I probably shared the wrong project. let me check.

@0xTim I'm so sorry to have taken up your time with the wrong project.
This is the one that has the failing tests: iCloud Drive - Apple iCloud

Ok I can see the issue - the problem you're hitting is that the compiler is picking VaporTesting's withApp instead of the one you've defined so it's never registering the routes. You can fix this pretty easily by getting Vapor Testing to call the configure function. If you change your test file to

@Suite("The MarsRover API should") struct MarsRoverAPITests {
    @Suite("when the create endpoint is correctly falled") struct CreateEndPointCorrectlyCalled {
        @Test("then it should return a status 201 (Created) message") func thenItShouldReturnAStatus201CreatedMessage() async throws {
            try await withApp(configure: configure) { app in
                try await app.testing().test(.POST, "v1/rovers") { res async in
                    #expect(res.status == .created)
                }
            }
        }
    }
    
    @Suite("when the get all rovers endpoint is correctly called") struct GetAllRoversEndPointCorrectlyCalled {
        @Test("then it should return a status 200 (OK) message") func thenItShouldReturnAStatus20OKMessage() async throws {
            try await withApp(configure: configure) { app in
                try await app.testing().test(.GET, "v1/rovers") { res async in
                    #expect(res.status == .ok)
                }
            }
        }
    }
}

All the tests pass. (Note the withApp(configure: configure) call) you can remove your withApp implementation

Yes, this solved it! :partying_face:

Looks like the documentation should be updated to reflect this change? I can create a PR if it helps.

That would be great thanks!

Draft PR: Update testing documentation to include withApp(configure:) method by maartene ยท Pull Request #1086 ยท vapor/docs ยท GitHub

1 Like