How to use the binary crate for integration tests

I'm extending on chapter 20 of the book, where I created a multi-threaded web server. ThreadPool implementation is independent of the kind of work our web server is doing, so we naturally separated them.

  • The web server was written in the binary crate src/bin/main.rs
  • The ThreadPool implementation went into src/lib.rs. Doing so makes the library crate the primary crate in the hello directory

Before I begin explaining the issue, here is the code located inside src/bin/main.rs:

Hint: See handle_connection

use std::net::{TcpStream, TcpListener};
use std::io::prelude::*;
use std::fs;
use hello::ThreadPool;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}


fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();
    
    let get = b"GET / HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "view/hello.html")
    } else { 
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "view/404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!("{}{}", status_line, contents);

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Note: The ThreadPool implementation is entirely written in src/lib.rs (no problems here)

Writing an integration test

One of the challenges at the end of the book is to write integration tests. I created a new directory called tests and made a file named tests/integration_test.rs (The tests directory is at the same level as src).

Inside integration_test.rs, I wanted to simulate a graceful shutdown by only accepting two requests before shutting down the server. (I was later planning on using the reqwest crate to simulate a client).

This is where I ran into an issue...

use hello::ThreadPool;
// Can't bring `handle_connection()` into scope
use reqwest;


#[test]
fn graceful_shutdown() {
    // use reqwest crate to simulate a client
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

I discovered that I am unable to bring the handle_connection() function into scope because it only exists in the binary (src/bin/main.rs)

I was thinking of ways around this, like moving handle_connection into lib.rs, but I feel that it doesn't belong with the ThreadPool implementation.

Any suggestions?

Thanks,
Connor

That's why I recommend to have lib.rs even for binary crates. With this pattern main.rs doesn't have any mods and only contains logics needed to run the binary itself like cli parsing and logger setup, all the core logics are implemented within the lib.rs and its submodules. Now you can crate_name::run(args) in main.rs, integration_test.rs, or even some downstream crates that want to embed your crate's functionality.

3 Likes