Anatomy of a Gem: Bane

Preview:

DESCRIPTION

Inside the design decisions of Bane, a test harness for sockets. This talk discusses the key design decisions of Bane, presents some code, and looks at some of Bane's automated tests.

Citation preview

Anatomy of a Ruby Gem

Bane !A test harness for server connections.

March 19, 2014 !

Daniel Wellman @wellman

dan@danielwellman.com

About Me

• Extreme Programming and Test-Driven Development since 2000

• Ruby since 2005, Rails 1.x in 2006

• Helping teams deliver working software safely and reliably for eight years by pairing and coaching (TDD, refactoring, agile development practices, etc.)

Systems Talk to Others

Our Application

FacebookGoogle Authentication

Internal domain servicesPayment

Processors

Sockets

e.g. localhost:3000

Our Application

Stock Quote Server

GOOG

Price: $465.87

Normal Response

Eventually Some System Will Behave

Unexpectedly

Our Application

Stock Quote Server

GOOG

Nobody Home

Our Application

Stock Quote Server

GOOG

... zzz ...

No Response

Our Application

Stock Quote Server

GOOG

!?

Unexpected Response

So What?

Our Application

Stock Quote Server

GOOG

... zzz ...

Photo by Ed Schipul

Our Application

Bane

GOOG

not listening

... zzz ...

Use Bane!

>  gem  install  bane

Installation

Demo

Bane’s Goal: !

Have the common behaviors at your fingertips.

Design Strategy !

Don’t require any additional gems, so we can easily run

anywhere

Behaviors

My Goal: !

I don’t want to write my own server to get this project started

GServer !

(class in the Ruby standard library)

Any kind of protocol, from HTTP to SMTP to

something custom

require 'gserver'# # A server that returns the time in # seconds since 1970.# class TimeServer < GServer def initialize(port=10001, *args) super(port, *args) end def serve(io) io.puts(Time.now.to_s) endendserver = TimeServer.newserver.start

Great! I want to make some behaviors!

class FixedResponse < GServer def serve(io) io.write “Hello, World!” endend

Subclass?

class NeverRespond < GServer def serve(io) # ... endend

class RandomResponse < GServer def serve(io) # ... endend

I’d prefer not

class FixedResponse < GServer def serve(io) io.write “Hello, World!” endend

Testing?

Start a Server for Every Test?

or Test a Private Method?

class FixeResponseTest < Test::Unit::TestCase def test_sends_the_same_message_every_time server = FixedResponse.new(3000) server.start response = # connect to port 3000 and query assert_equal "Hello, World!”, response server.stop end end

Start a Server for Every Test?

• Uses real I/O • Testing GServer

Over and Over

Test a Private Method?

class FixedResponseTest < Test::Unit::TestCase def test_sends_the_same_message_every_time server = FixedResponse.new(3000) # call the serve() method directly server.serve(fake_connection) assert_equal "Hello, World!”, fake_connection.string endend

• Coupled to implementation

Test through the object’s public interface

Delegate!

BehaviorServer FixedResponsehas a

class BehaviorServer < GServer def initialize(port, behavior, host) super(port, host) @behavior = behavior # ... end def serve(io) @behavior.serve(io) endend

class FixedResponse def serve(io) io.write "Hello, world!" end

end

server = BehaviorServer.new(3000, FixedResponse.new, '127.0.0.1')

Test Behavior in Isolation

class FixedResponseTest < Test::Unit::TestCase def test_sends_the_same_message_every_time behavior = FixedResponse.new behavior.serve(fake_connection) assert_equal "Hello, World!", fake_connection.string endend

But how do we know the whole thing works?

TDD Loop

From Freeman & Pryce, “Growing Object-Oriented Software: Guided by Tests”

Acceptance Testsclass BaneAcceptanceTest < Test::Unit::TestCase TEST_PORT = 4000 def test_uses_specified_port_and_server run_server_with(TEST_PORT, FixedResponse) do with_response_from TEST_PORT do |response| assert !response.empty? end end end

# … !end

Acceptance Test Helpers

def run_server_with(port, behavior, &block) # ...enddef with_response_from(port) begin connection = TCPSocket.new "localhost", port yield connection.read ensure connection.close if connection endend

Write Tests in the Language of the Problem Domain

This is almost the production code…

class FixedResponse def serve(io) io.write “Hello, World!” endend

Programmatic Userequire 'bane'include Bane

behavior = Behaviors::FixedResponse.new( message: "Shall we play a game?”) launcher = Launcher.new([ BehaviorServer.new(3000, behavior)])launcher.start

# Sends a static response.# # Options:# - message: The response message to send. Default: "Hello, world!"class FixedResponse def initialize(options = {}) @options = {message: "Hello, world!”} .merge(options) end def serve(io) io.write @options[:message] endend

More Acceptance Testsdef test_serves_http_requests run_server_with(TEST_PORT, HttpRefuseAllCredentials) do assert_match /401/, status_returned_from( "http://localhost:#{TEST_PORT}/url") endend

def status_returned_from(uri) begin open(uri).read rescue OpenURI::HTTPError => e return e.message end flunk "Should have refused access"end

class HttpRefuseAllCredentials UNAUTHORIZED_RESPONSE_BODY = <<EOF<!DOCTYPE html><html>… </html>EOF def serve(io) io.gets response = NaiveHttpResponse.new( 401, "Unauthorized", “text/html", UNAUTHORIZED_RESPONSE_BODY) io.write(response.to_s) endend

Close Immediately

# Closes the connection immediately # after a connection is made.class CloseImmediately def serve(io) # do nothing endend

Echo Response

class EchoResponse def serve(io) while(input = io.gets) io.write(input) end io.close endend

NeverRespond

class NeverRespond def serve(io) sleep endend

NeverRespond

class NeverRespond def serve(io) while !io.closed? sleep 1 end endend

Photo by Sean T. Allen

New Behavior: Server is Not Listening

Socket Lifecycle

1. create

2. bind

3. listen

4. accept

5. close

Never Listen

@server = Socket.new(:INET, :STREAM) address = Socket.sockaddr_in(port, host)@server.bind(address) # Note that we never call listen

Clients that try to connect get an ECONNREFUSED error

How do we fit this into our GServer-based

code?

require 'gserver'# # A server that returns the time in # seconds since 1970.# class TimeServer < GServer def initialize(port=10001, *args) super(port, *args) end def serve(io) io.puts(Time.now.to_s) endendserver = TimeServer.newserver.start

X It’s too late in the socket lifecycle!

class NeverListen def initialize(port, host = Services::LOCALHOST) @port = port @host = host end def start @server = Socket.new(:INET, :STREAM) address = Socket.sockaddr_in(port, host) @server.bind(address) log 'started' end def stop @server.close log 'stopped' end ! # … end

Now We’re Two…

• Small server-independent behaviors that require a GServer (or something) to manage their lifecycle

• Behaviors that use low-level sockets and manage their own lifecycle

…. called what?

Services?

Behaviors?

Two Groups to Name• NeverRespond

• CloseImmediately

• FixedResponse

• EchoResponse

• RandomResponse

• …

• NeverListen

• FullListenQueue

• BehaviorServer

http://github.com/danielwellman/bane

Bane

dan@danielwellman.comTwitter: @wellman