A unit testing problem

Hello…

I’m trying to figure out how to test something
properly and I’m seeking your input.

I’m talking to a device via a serial port. The
protocol is described in terms of hex, so for a
first pass I was using a sort of mock object
serial port stub:

class SerialPort < IO

attr_accessor :read_timeout, :sync

def initialize(*args)
end

def putc(ch)
  STDOUT.printf "PC:    0x%02x\n", ch
end

def getc
  STDOUT.print "CM11A: "
  n = Integer(STDIN.gets)
  exit if n==999
  n
end

end

And I was pretending to be the receiving device and
talking back and forth to the PC in hex:

C:\Windows\Desktop\projects\domo>ruby x10.rb
CM11A: 0x5a
PC: 0xc3
PC: 0x2e
PC: 0x66
CM11A: 0x57
PC: 0x2e
PC: 0x66
CM11A: 0x94
PC: 0x00

But of course, interactive tests are not good in
the long term.

How would you test this? For my first pass, I
didn’t have to make any changes to my X10 class;
I just substituted a fake SerialPort class.

I want to use StringIO, but it’s not obvious to me
how to proceed.

I thought about sending fake arguments to the
SerialPort constructor, which would then set up
StringIO objects… it was getting rather too
complex for my tastes. As I said, I don’t want
to change the caller for the sake of testing.

What do you think?

Hal

···


Hal Fulton
hal9000@hypermetrics.com

Could you use a couple of IO.pipe objects instead, and run your mock object
as a separate thread? (or a separate process)

ISTM that the mock object will want to receive characters down this pipe one
at a time, when it has enough to process a command then it will do it
and send the result back. Hence there has to be some buffering ‘slack’
between the two, and it needs to be able to run asynchronously.

Actually, for simplicity you could write your device simulator as a separate
program, which you run like this:

f = IO.pipe(“/path/to/my/simulator”)

now you can just read and write to f as if it were as serial port?

You could create it in the ‘setup’ part of Test::Unit.

Just a thought,

Brian.

···

On Thu, Jun 05, 2003 at 02:58:23PM +0900, Hal E. Fulton wrote:

How would you test this? For my first pass, I
didn’t have to make any changes to my X10 class;
I just substituted a fake SerialPort class.

I want to use StringIO, but it’s not obvious to me
how to proceed.

But of course, interactive tests are not good in
the long term.

Agreed.

How would you test this? For my first pass, I
didn’t have to make any changes to my X10 class;
I just substituted a fake SerialPort class.

I assume in your tests you can script the entire exchange. In other
words, when you wish to test a scenario, you know exactly what you will
send and when repsonses you will get back.

If that’s the case, you could do something like this …

def test_startup_protocol
port = MockSerialPort.new(
:put, 0x80, 0x81,
:get, 0x00, 0x01,
:put, 0xff)
cm11a = CM11A.new(port)
cm11a.startup
end

The MockSerialPort object might look something like this …

class MockSerialPort
include Test::Unit::Assertions

def initialize(*script)
@mode = :put
@script = script
end

def putc(byte)
expected = get_next
assert_equal :put, @mode
assert_equal expected, byte, “while putting”
end

def getc
expected = get_next
assert_equal :get, @mode
expected
end

def get_next
while val = @script.shift
case val
when :put
@mode = val
when :get
@mode = val
when Integer
return val
when nil
return val
end
end
end
end

···

On Thu, 2003-06-05 at 01:58, Hal E. Fulton wrote:


– Jim Weirich jweirich@one.net http://onestepback.org

“Beware of bugs in the above code; I have only proved it correct,
not tried it.” – Donald Knuth (in a memo to Peter van Emde Boas)

“Hal E. Fulton” hal9000@hypermetrics.com wrote in message

I’m talking to a device via a serial port. The
protocol is described in terms of hex, so for a
first pass I was using a sort of mock object
serial port stub:

[snip]

How about using the Test::Unit::MockObject framework like this:

require ‘test/unit/mock’

class SerialPort < IO

attr_accessor :read_timeout, :sync

def initialize(*args)
end

def putc(ch)
STDOUT.printf “PC: 0x%02x\n”, ch
end

def getc(s)
STDOUT.print "CM11A: "
s = STDIN.gets
n = Integer(s)
exit if n==999
n
end

end

mockSerial = Test::Unit::MockObject( SerialPort ).new
mockSerial.setReturnValues( :putc => Proc::new{|ch| STDOUT.print “PC:
0x%02x\n” % ch},
:getc => Proc::new{|s| STDOUT.print "CM11A: ";
(s == “999”) ? exit :
Integer(s) }
)

mockSerial.setCallOrder( :getc, :putc, :putc, :putc,
:getc, :putc , :putc, :putc, :putc,
:getc)
mockSerial.strictCallOrder = true
mockSerial.activate

mockSerial.getc(“0x5a”)
mockSerial.putc(0xc3)
mockSerial.putc(0x2e)
mockSerial.putc(0x66)

mockSerial.getc(“0x57”)
mockSerial.putc(0x2e)
mockSerial.putc(0x66)
mockSerial.putc(0x94)
mockSerial.putc(0x00)

mockSerial.getc(“0x2d”)
#mockSerial.getc(“999”)

mockSerial.verify
puts “Call trace:\n\t” + mockSerial.callTrace.join(“\n\t”)

···

#---------------------------------------------------------
Produces:

CM11A: PC: 0xc3
PC: 0x2e
PC: 0x66
CM11A: PC: 0x2e
PC: 0x66
PC: 0x94
PC: 0x00
CM11A: Call trace:
getc( “0x5a” ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:37
putc( 195 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:38
putc( 46 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:39
putc( 102 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:40
getc( “0x57” ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:42
putc( 46 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:43
putc( 102 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:44
putc( 148 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:45
putc( 0 ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:46
getc( “0x2d” ) at 0.00000 seconds from C:/atest/tst_mock_serial.rb:48
Loaded suite C:/atest/tst_mock_serial
Started

Finished in 0.0 seconds.

0 tests, 0 assertions, 0 failures, 0 errors

Could you use a couple of IO.pipe objects instead, and run your mock
object
as a separate thread? (or a separate process)

ISTM that the mock object will want to receive characters down this pipe
one
at a time, when it has enough to process a command then it will do it
and send the result back. Hence there has to be some buffering ‘slack’
between the two, and it needs to be able to run asynchronously.

Actually, for simplicity you could write your device simulator as a
separate
program, which you run like this:

f = IO.pipe(“/path/to/my/simulator”)

now you can just read and write to f as if it were as serial port?

Hmmm. This sounds neat. I’ve actually fallen out
of the habit of using IO.pipe, since it quit
working for awhile on Windows. (It may work now,
I don’t know.) But now I’m migrating to Linux,
so why not?

Yet I don’t want to build too much intelligence
into the simulator. I’d have to parameterize it
somehow, in order to handle different scenarios.

I will keep this in mind, but I’m thinking I will
try Jim’s approach.

Thanks,
Hal

···

----- Original Message -----
From: “Brian Candler” B.Candler@pobox.com
To: “ruby-talk ML” ruby-talk@ruby-lang.org
Sent: Thursday, June 05, 2003 6:54 AM
Subject: Re: A unit testing problem…

[snip]

Yes, that assumption is valid.

I think I like this approach. I will play with it and
see if I hit any snag.

Thanks,
Hal

···

----- Original Message -----
From: “Jim Weirich” jweirich@one.net
To: “ruby-talk ML” ruby-talk@ruby-lang.org
Sent: Thursday, June 05, 2003 7:32 AM
Subject: Re: A unit testing problem…

I assume in your tests you can script the entire exchange. In other
words, when you wish to test a scenario, you know exactly what you will
send and when repsonses you will get back.

If that’s the case, you could do something like this …

How about using the Test::Unit::MockObject framework like this:

That is very interesting… I’ve never used
that framework.

My most recent code is at the bottom of this
page: http://rubyhacker.com/domo.html

I’m not sure that MockObject would be any
better than this… please feel free to
point out where I’m wrong.

Thanks,
Hal

···

----- Original Message -----
From: “Shashank Date” sdate@everestkc.net
Newsgroups: comp.lang.ruby
To: “ruby-talk ML” ruby-talk@ruby-lang.org
Sent: Saturday, June 07, 2003 12:09 AM
Subject: Re: A unit testing problem…