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")
    }
}
3 Likes

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

Terms of Service

Privacy Policy

Cookie Policy