JoeG
JoeG

Reputation: 7652

Test Groovy class that uses System.console()

I have a groovy script that asks some questions from the user via a java.io.console object using the readline method. In addition, I use it to ask for a password (for setting up HTTPS). How might I use Spock to Unit Test this code? Currently it complains that the object is a Java final object and can not be tested. Obviously, I'm not the first one trying this, so thought I would ask.

A sketch of the code would look something like:

class MyClass {

   def cons

   MyClass() {
     cons = System.console()
   }

   def getInput = { prompt, defValue ->
     def input = (cons.readLine(prompt).trim()?:defValue)?.toString()
     def inputTest = input?.toLowerCase()
     input
   }
}

I would like Unit Tests to test that some mock response can be returned and that the default value can be returned. Note: this is simplified so I can figure out how to do the Unit Tests, there is more code in the getInput method that needs to be tested too, but once I clear this hurdle that should be no problem.

EDITED PER ANSWER BY akhikhl Following the suggestion, I made a simple interface:

interface TestConsole {
    String readLine(String fmt, Object ... args)
    String readLine()
    char[] readPassword(String fmt, Object ... args)
    char[] readPassword()
}

Then I tried a test like this:

def "Verify get input method mocking works"() {

    def consoleMock = GroovyMock(TestConsole)
    1 * consoleMock.readLine(_) >> 'validResponse'

    inputMethods = new MyClass()
    inputMethods.cons = consoleMock

    when:
    def testResult = inputMethods.getInput('testPrompt', 'testDefaultValue')
    then:
    testResult == 'validResponse'
}

I opted to not alter the constructor as I don't like having to alter my actual code just to test it. Fortunately, Groovy let me define the console with just a 'def' so what I did worked fine.

The problem is that the above does not work!!! I can't resist - this is NOT LOGICAL! Spock gets 'Lost' in GroovyMockMetaClass somewhere. If I change one line in the code and one line in the test it works.

Code change:

From:
    def input = (cons.readLine(prompt).trim()?:defValue)?.toString()
To: (add the null param)
    def input = (cons.readLine(prompt, null).trim()?:defValue)?.toString()

Test change:

From:
    1 * consoleMock.readLine(_) >> 'validResponse'
To: (again, add a null param)
    1 * consoleMock.readLine(_, null) >> 'validResponse'

Then the test finally works. Is this a bug in Spock or am I just out in left field? I don't mind needing to do whatever might be required in the test harness, but having to modify the code to make this work is really, really bad.

Upvotes: 4

Views: 1357

Answers (1)

akhikhl
akhikhl

Reputation: 2578

You are right: since Console class is final, it could not be extended. So, the solution should go in another direction:

  1. Create new class MockConsole, not inherited from Console, but having the same methods.

  2. Change the constructor of MyClass this way:

    MyClass(cons = null) { this.cons = cons ?: System.console() }

  3. Instantiate MockConsole in spock test and pass it to MyClass constructor.

update-201312272156

I played with spock a little bit. The problem with mocking "readLine(String fmt, Object ... args)" seems to be specific to varargs (or to last arg being a list, which is the same to groovy). I managed to reduce a problem to the following scenario:

Define an interface:

interface IConsole {
  String readLine(String fmt, Object ... args)
}

Define test:

class TestInputMethods extends Specification {

  def 'test console input'() {
    setup:
    def consoleMock = GroovyMock(IConsole)
    1 * consoleMock.readLine(_) >> 'validResponse'

    when:
    // here we get exception "wrong number of arguments":
    def testResult = consoleMock.readLine('testPrompt')

    then:
    testResult == 'validResponse'
  }
}

this variant of test fails with exception "wrong number of arguments". Particularly, spock thinks that readLine accepts 2 arguments and ignores the fact that second argument is vararg. Proof: if we remove "Object ... args" from IConsole.readLine, the test completes successfully.

Here is Workaround for this (hopefully temporary) problem: change the call to readLine to:

def testResult = consoleMock.readLine('testPrompt', [] as Object[])

then test completes successfully.

I also tried the same code against spock 1.0-groovy-2.0-SNAPSHOT - the problem is the same.

update-201312280030

The problem with varargs is solved! Many thanks to @charlesg, who answered my related question at: Spock: mock a method with varargs

The solution is the following: replace GroovyMock with Mock, then varargs are properly interpreted.

Upvotes: 3

Related Questions