Unit testing argument parsing?

I've just started building a little CLI tool in Xcode on macOS, but I'm trying to make it as platform-agnostic as possible. To that end I found the nice ArgumentParser package.

But I'm finding it would be nice to be able to run automated XCTestCase cases against the argument parsing, to make sure I’ve specified it correctly. How might I go about doing this?

It's a bit odd, since Xcode didn't create any unit tests when I instantiated the macOS CLI template, and even after adding a couple of Swift Package Dependencies, it the project doesn't have a Package.swift file. I suppose to be properly cross platform I should add one, but I don't want to fight Xcode right now and I'm worried about rocking its little boat.

What's the best way to unit test the parsing? Thanks!

We very much designed the library with this in mind. You can use the parseAsRoot() function in unit tests to do this.

Let’s say you have a RootCommand with a PingCommand as a sub-command. You’d then be able to write a test like this:

func testParsing() throws {
    let ping = try XCTUnwrap(RootCommand.parseAsRoot([
        "ping",
        "--versbose",
        "10",
    ]) as? PingCommand)
    XCTAssert(ping.verbose)
    XCTAssertEqual(ping.count, 10)
}

— and similarly check that parseAsRoot() throws if the options are invalid.

If you’re not using nested commands, but “just” ParsableArguments, you’d use parse() instead.

I hope this helps.

5 Likes

Yeah that's great! Too bad for the as?, but that'll do just fine.

Well, it’s a test, so you want to check that it turned into a PingCommand. You can clean that up, though, by creating a helper:

final class ArgumentParsingTests: XCTestCase {
    func parse<A>(_ type: A.Type, _ arguments: [String]) throws -> A where A: ParsableCommand {
        return try XCTUnwrap(RootCommand.parseAsRoot(arguments) as? A)
    }
}

extension ArgumentParsingTests {
    func testParsingDefaults() throws {
        let ping = try parse(PingCommand.self, [
            "ping"
        ])
        XCTAssertFalse(ping.verbose)
        XCTAssertEqual(ping.count, 4)
    }

    func testParsingPingVerbose() throws {
        let ping = try parse(PingCommand.self, [
            "ping", "--verbose", "10"
        ])
        XCTAssert(ping.verbose)
        XCTAssertEqual(ping.count, 10)
    }
    
    …

    func testParsingSend() throws {
        let send = try parse(SendCommand.self, [
            "send", "Hello, World!",
        ])
        XCTAssert(send.text, "Hello, World")
    }
}
4 Likes

Oh of course, that makes sense. I was calling it directly as it's the only command.

For those of us who are still for whatever reason tightly bound to Xcode-centric building and testing, how do we add a test target that can access .parseAsRoot()? Are we forced, as others have suggested, to move all our code into a static library target, now testable, with the CLI executable target as just a thin shim to call it?

Furthermore, if using parseAsRoot(), how can we test assertions about standard and error output from the tool?

Or is there another way? I see that ArgumentParser's TestHelpers.swift extends XCTest adding AssertExecuteCommand which executes the CLI binary directly, but I have no idea how to add an Xcode test target that would invoke it - seems like SPM only.