3

I'm trying to build some integration tests for a small API I've built using the Saturn framework.

The API is built with the usual Saturn computation expressions such as application, controller, router etc.

But in order to build integration tests I need to replace the application computation expression (ce) and hand craft a WebHostBuilder.

My application ce looks like this:

module Server 

let app =
    application {
        url ("http://0.0.0.0:" + port.ToString() + "/")
        use_router apiRouter
        memory_cache
        service_config configureSerialization
        use_gzip
        use_config (fun _ ->
            System.Environment.CurrentDirectory <- (System.Reflection.Assembly.GetExecutingAssembly()).Location
                                                   |> Path.GetDirectoryName
            let configurationRoot =
                ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appSettings.json")
                    .Build()
            let appConfig = FsConfig.AppConfig(configurationRoot)

            let dbPath =
                match appConfig.Get<AppSettings>() with
                | Ok settings when settings.DbConfig.Database.Contains(":memory:") -> settings.DbConfig.Database
                | Ok settings -> Path.Combine(System.Environment.CurrentDirectory, settings.DbConfig.Database)
                | Error _ -> failwithf "Invalid database path"
            { connectionString = dbPath |> sprintf "DataSource=%s;Version=3" })
    } 

With a router and on one of the controllers...

let cartController =
    controller {
        create createCartAction
        show getCartAction
        delete deleteCartAction
        subController "/items" cartItemsController
    }

let apiRouter =
    router {
        not_found_handler (setStatusCode 404 >=> text "Not found")
        pipe_through apiPipeline
        forward "/cart" cartController
    }

The code above is in my API project, the code with integration test below is in a second project. The latter has a project reference to the former.

let configureApp (app : IApplicationBuilder) =
    app.UseGiraffe(Server.apiRouter) \\<-- The problem is here. Server.apiRouter is null!

let configureServices (services : IServiceCollection) =
    services.AddGiraffe() |> ignore

let builder = WebHostBuilder()
                .UseContentRoot(contentRoot) 
                .Configure(Action<IApplicationBuilder> configureApp)
                .ConfigureServices(configureServices)

let testServer = new TestServer(builder)

let client = testServer.CreateClient()

let! response = client.GetAsync "/"

test <@ HttpStatusCode.OK = response.StatusCode @> 

When the test is run it fails with the following exception:

System.InvalidOperationException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'A suitable constructor for type 'Giraffe.Middleware+GiraffeMiddleware' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.' 

The problem appears to be with the line app.UseGiraffe(Server.apiRouter). apiRouter is defined in the Server module of the API project - but when this code runs in the test project Server.apiRouter is null.

If however I move the test code into the same project as the API - the test works perfectly.

Why would the apiRouter computation expression be null if called from the test project?

Simon Lomax
  • 8,714
  • 8
  • 42
  • 75

1 Answers1

0

It turned out that this was nothing to do with Saturn at all. It's all to do with the way that F# initiates modules. The answer can be found here

As suggested in the referenced post. I simply added an [<EntryPoint>] function and everything worked as expected.

Simon Lomax
  • 8,714
  • 8
  • 42
  • 75