rdxdkr
rdxdkr

Reputation: 1189

How to test CLI arguments with clap in Rust?

What would be the most idiomatic way to write tests for a CLI program using clap? I'm currently doing it like this:

#[derive(Debug, Parser)]
#[clap(author, version, about)]
pub struct StructArgs {
    #[clap(subcommand)]
    pub command_type: CommandType,
}

#[derive(Debug, Subcommand)]
pub enum CommandType {
    Command1(Command1Args),
    ...
}

#[derive(Debug, Args)]
pub struct Command1Args {
    pub field: String,
    ...
}

impl Command1Args {
    ...
}

#[test]
fn test_do_stuff() {
    let args = StructArgs::try_parse_from(
        std::iter::once("<PROGRAM NAME>")
        .chain(
            ["<ARG 1>", ..., "<ARG n>"]
            .iter()
            .cloned()
        )
    );

    if let CommandType::Command1(command1_args) = args.command_type {
        // do stuff with command1_args
    } else {
        panic!();
    }
}

Basically I pass to clap an iterator of arguments, then I check if the parsed command structure matches with the CommandType I expect, and I proceed to test its methods and internal state. The panic in the else branch is for failing the test if for some reason I get an unexpected CommandType, meaning most likely that I have written something wrong in the iterator.

Can this be improved further?

Upvotes: 8

Views: 3863

Answers (1)

Markus
Markus

Reputation: 2572

Chayim certainly has a point when he says you should not test Clap. But there are scenarios where this is not the case and writing unit tests for a CLI parser implemented with clap does make sense.

In my case, I want to define a group of arguments that is only required if a certain flag is set, and if set, all arguments of that group are required. This is not a trivial thing to define in Clap, and it makes sense testing it, for example like this (using Clap 4.4):

#[cfg(test)]
mod tests {

  use super::*;

  #[test]
  fn oidc_all_or_nothing() {
    /* Call it without any OIDC args, just graqphiql; must work */
    if let Err(err) = AppConfig::try_parse_from(vec!["aisrv", "--enable-graphiql"].iter()) {
      panic!("Just --enable-graphiql failed: {err}");
    }

    /* Call with only one OIDC arg, must fail */
    if AppConfig::try_parse_from(vec!["aisrv", "--oidc-idp-url", "tescht"].iter()).is_ok() {
      panic!("Just one OIDC arg did not fail!");
    }

    /* Call with all OIDC args, must succeed */
    if let Err(err) = AppConfig::try_parse_from(
      vec![
        "aisrv",
        "--oidc-idp-url",
        "tescht",
        "--oidc-super-admin-user",
        "tescht",
        "--oidc-super-admin-password",
        "tescht",
        "--oidc-super-admin-client-id",
        "tescht",
        "--oidc-super-admin-client-secret",
        "tescht",
        "--oidc-aud",
        "tescht",
      ]
      .iter(),
    ) {
      panic!("All OIDC args passed, but still fails: {err}");
    }
  }
}

Upvotes: 4

Related Questions