URL Shortener with Rust, Svelte, & AWS (3/): Testing

URL Shortener with Rust, Svelte, & AWS (3/): Testing

In the last post, we created a simple URL-shortener API and tested it manually with curl and a browser. In this post, we will use Rust's integrated unit-testing features to allow testing with a single command, and then automate testing with GitHub Actions.

Creating a test module

In Rust, we can create unit-tests which allow us to test individual parts of our application. To get started, create a module called tests in your main.rs file, and add the cfg(test) macro.

#[cfg(test)]
mod tests {
    // ...
}

Now we have created a tests module, we can add tests inside it with the test macro.

#[test]
fn simple_demo_test() {
    let x = 1 + 1;
    assert_eq!(x, 3)
}

By running cargo test, you should see that the test fails as expected:

running 1 test
test tests::simple_demo_test ... FAILED

By changing the 3 to 2 and rerunning cargo test, you should be able to see it pass as expected.

Valid Request Testing

The Rocket crate includes functionality which allows us to write unit tests for our applications (you can find the full docs here.

In particular, we will use the rocket::local::blocking::Client struct to simulate requests to our API. We will first use it to check that valid requests are accepted correctly.

#[test]
fn valid_requests() {
    // ...
}

First, we create a client that is able to make requests to our Rocket.

let client = Client::tracked(rocket())
    .expect("valid rocket instance");

Next, we make a POST request to shorten a url, and check that the response status is Ok.

let response = client
    .post("/api/shorten?url=https://duck.com")
    .dispatch();
assert_eq!(response.status(), Status::Ok);

We can then attempt to extract the key which was returned from the response body, panicking if the key could not be parsed successfully.

let key: u32 = response
    .into_string()
    .expect("body")
    .parse()
    .expect("valid u32");

Now we can make a GET request to the shortened URL, and check that the response is a redirect to the original URL.

let response = client.get(format!("/{}", key)).dispatch();

assert_eq!(response.status(), Status::SeeOther);

let redirect = response
    .headers()
    .get_one("Location")
    .expect("location header");

assert_eq!(redirect, "https://duck.com")

Running the full test with cargo run should result in a successfull pass.

Invalid Request Testing

In the current edition of our API, there are two ways that a request can fail - we should check these both.

First, we will create a test to check that a missing URL parameter throws an error.

#[test]
fn empty_url() {
    let client = Client::tracked(rocket())
        .expect("valid rocket instance");
    let response = client.post("/api/shorten?url=").dispatch();
    assert_eq!(response.status(), Status::BadRequest);
}

And finally we will create a test to ensure that the server returns Not Found to an invalid shortened URL.

#[test]
fn invalid_url() {
    let client = Client::tracked(rocket())
        .expect("valid rocket instance");
    let response = client.post("/123").dispatch();
    assert_eq!(response.status(), Status::NotFound);
}

Both these tests should pass without issue.

GitHub Actions

Now we have created our tests, we can write a GitHub actions script to automatically run our tests whenever we push to the main branch. Create a file with the path .github/workflows/test.yml, and add the following script:

name: Test
on:
  push:
    branches: [main]
env:
  CARGO_TERM_COLOR: always
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build
        run: cargo build
      - name: Run tests
        run: cargo test

When you push your code to GitHub, you should be able to see your tests run (and hopefully pass) in the "Actions" tab of your repo.

alt text

If you have an issue, you can compare your code to the part-3 tag of my repo.

That's all for this post! In the next one, we will work on containerizing our application with Docker. Make sure to click the "Follow" button if you want to be alerted when the next part is available!

Footnote

If you enjoyed reading this, then consider dropping a like or following me:

I'm just starting out, so the support is greatly appreciated!

Disclaimer - I'm a (mostly) self-taught programmer, and I use my blog to share things that I've learnt on my journey to becoming a better developer. Because of this, I apologize in advance for any inaccuracies I might have made - criticism and corrections are welcome!