Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File Structure and Testing #13

Closed
phortx opened this issue Aug 7, 2017 · 4 comments
Closed

File Structure and Testing #13

phortx opened this issue Aug 7, 2017 · 4 comments
Labels

Comments

@phortx
Copy link

phortx commented Aug 7, 2017

First I want to thank you for your work at this project, it helps me a lot! Kudos!

Just one question: How to actually write specs for a CLI tool built with commander?

I have currently one sourcefile src/some_tool.cr which contains:

module SomeTool
end

cli = Commander::Command.new do |cmd|
  # ...
end

Commander.run cli, ARGV

My spec_helper.cr contains:

require "spec"
require "../src/some_tool"

Which immediately runs the commander logic.

I'm thankful for any advice :)

@mrrooijen
Copy link
Owner

mrrooijen commented Aug 7, 2017

Thanks for the kind words!

What I would do is create the following file structure:

src
├── compile.cr
├── example
│   ├── cli.cr
│   └── version.cr
└── example.cr

With the following contents:

src/example.cr

require "./example/*"

module Example
end

src/compile.cr

require "./example"
Example::CLI.run(ARGV)

src/example/version.cr

module Example
  VERSION = "0.1.0"
end

src/example/cli.cr

require "commander"

module Example::CLI
  extend self

  def config
    Commander::Command.new do |cmd|
      cmd.use = "my_program"
      cmd.long = "my program's (long) description."

      cmd.flags.add do |flag|
        flag.name = "env"
        flag.short = "-e"
        flag.long = "--env"
        flag.default = "development"
        flag.description = "The environment to run in."
      end

      cmd.flags.add do |flag|
        flag.name = "port"
        flag.short = "-p"
        flag.long = "--port"
        flag.default = 8080
        flag.description = "The port to bind to."
      end

      cmd.flags.add do |flag|
        flag.name = "timeout"
        flag.short = "-t"
        flag.long = "--timeout"
        flag.default = 29.5
        flag.description = "The wait time before dropping the connection."
      end

      cmd.flags.add do |flag|
        flag.name = "verbose"
        flag.short = "-v"
        flag.long = "--verbose"
        flag.default = false
        flag.description = "Enable more verbose logging."
      end

      cmd.run do |options, arguments|
        options.string["env"]    # => "development"
        options.int["port"]      # => 8080
        options.float["timeout"] # => 29.5
        options.bool["verbose"]  # => false
        arguments                # => Array(String)
        puts cmd.help            # => Render help screen
      end

      cmd.commands.add do |cmd|
        cmd.use = "kill <pid>"
        cmd.short = "Kills server by pid."
        cmd.long = cmd.short
        cmd.run do |options, arguments|
          arguments # => ["62719"]
        end
      end
    end
  end

  def run(argv)
    Commander.run(config, argv)
  end
end

Now when you run your specs with crystal spec it won't call the Commander.run(config, argv) command. When testing individual commands, I recommend just keeping each individual command body extremely small, and call functions that you can test in isolation.

Avoid doing this:

cmd.commands.add do |cmd|
  cmd.use = "kill <pid>"
  cmd.short = "Kills server by pid."
  cmd.long = cmd.short
  cmd.run do |options, arguments|
    # a lot of lines of code and logic here
    # related to killing the <pid> process.
  end
end

Instead, do something like this:

cmd.commands.add do |cmd|
  cmd.use = "kill <pid>"
  cmd.short = "Kills server by pid."
  cmd.long = cmd.short
  cmd.run do |options, arguments|
    # Only validate the CLI input here.
    # You could also delegate this validation logic to a separate testable and reusable function.
    # For simple cases like this it might not be necessary. It's up to you.
    if pid = arguments.first?
      # Delegate the actual work for killing a process to separate function,
      # which makes testing the process-killing logic easier, without having to go through Commander.
      Example::Process.kill(pid.to_i)
    else
      # Notify user of erroneous input.
      puts "Missing <pid> argument."
    end
  end
end

Then create a spec spec/example/process_spec.cr to test the Example::Process.kill(pid) function, and define Example::Process in src/example/process.cr. This makes your code much easier to test.

Then, to compile your program with the CLI built-in, I'd create a separate file like src/compile.cr (or whatever you'd like to name it) containing just:

require "./example"
Example::CLI.run(ARGV)

Then compile your program with:

crystal build --release -o example ./src/compile.cr

If, in addition to just testing individual functions, you do wish to actually run the CLI programmatically for certain tests, you can call Example::CLI.run(["kill", "18271"]) directly. For example:

require "./spec_helper"

describe Example::Process do

  it "kills a process" do
    process = Process.fork do
      sleep 1000
    end

    Example::CLI.run(["kill", process.pid.to_s])

    Process.exists?(process.pid).should eq(false)
  end
end

Hope this helps!

@phortx
Copy link
Author

phortx commented Aug 7, 2017

WOW! What a comprehensive answer 😮

Thank you very much, I'll try that soon :)

@mrrooijen
Copy link
Owner

Sure thing! Let me know if you have any further questions.

@phortx
Copy link
Author

phortx commented Aug 7, 2017

I've tried that and it works like a charm, thank you very much! <3

@phortx phortx closed this as completed Aug 7, 2017
@mrrooijen mrrooijen changed the title Specs Testing and File Structure Feb 10, 2019
@mrrooijen mrrooijen pinned this issue Feb 10, 2019
@mrrooijen mrrooijen changed the title Testing and File Structure File Structure and Testing Feb 10, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants