anatomy of a gem: bane

57
Anatomy of a Ruby Gem Bane A test harness for server connections. March 19, 2014 Daniel Wellman @wellman [email protected]

Upload: daniel-wellman

Post on 05-Dec-2014

263 views

Category:

Technology


0 download

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.

TRANSCRIPT

Page 1: Anatomy of a Gem: Bane

Anatomy of a Ruby Gem

Bane !A test harness for server connections.

March 19, 2014 !

Daniel Wellman @wellman

[email protected]

Page 2: Anatomy of a Gem: Bane

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.)

Page 3: Anatomy of a Gem: Bane

Systems Talk to Others

Our Application

FacebookGoogle Authentication

Internal domain servicesPayment

Processors

Page 4: Anatomy of a Gem: Bane

Sockets

e.g. localhost:3000

Page 5: Anatomy of a Gem: Bane

Our Application

Stock Quote Server

GOOG

Price: $465.87

Normal Response

Page 6: Anatomy of a Gem: Bane

Eventually Some System Will Behave

Unexpectedly

Page 7: Anatomy of a Gem: Bane

Our Application

Stock Quote Server

GOOG

Nobody Home

Page 8: Anatomy of a Gem: Bane

Our Application

Stock Quote Server

GOOG

... zzz ...

No Response

Page 9: Anatomy of a Gem: Bane

Our Application

Stock Quote Server

GOOG

!?

Unexpected Response

Page 10: Anatomy of a Gem: Bane

So What?

Our Application

Stock Quote Server

GOOG

... zzz ...

Page 11: Anatomy of a Gem: Bane
Page 12: Anatomy of a Gem: Bane

Photo by Ed Schipul

Page 13: Anatomy of a Gem: Bane

Our Application

Bane

GOOG

not listening

... zzz ...

Use Bane!

Page 14: Anatomy of a Gem: Bane

>  gem  install  bane

Installation

Page 15: Anatomy of a Gem: Bane

Demo

Page 16: Anatomy of a Gem: Bane

Bane’s Goal: !

Have the common behaviors at your fingertips.

Page 17: Anatomy of a Gem: Bane

Design Strategy !

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

anywhere

Page 18: Anatomy of a Gem: Bane

Behaviors

Page 19: Anatomy of a Gem: Bane
Page 20: Anatomy of a Gem: Bane

My Goal: !

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

Page 21: Anatomy of a Gem: Bane

GServer !

(class in the Ruby standard library)

Page 22: Anatomy of a Gem: Bane

Any kind of protocol, from HTTP to SMTP to

something custom

Page 23: Anatomy of a Gem: Bane

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

Page 24: Anatomy of a Gem: Bane

Great! I want to make some behaviors!

Page 25: Anatomy of a Gem: Bane

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

Page 26: Anatomy of a Gem: Bane

I’d prefer not

Page 27: Anatomy of a Gem: Bane

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

Testing?

Page 28: Anatomy of a Gem: Bane

Start a Server for Every Test?

or Test a Private Method?

Page 29: Anatomy of a Gem: Bane

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

Page 30: Anatomy of a Gem: Bane

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

Page 31: Anatomy of a Gem: Bane

Test through the object’s public interface

Page 32: Anatomy of a Gem: Bane

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')

Page 33: Anatomy of a Gem: Bane

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

Page 34: Anatomy of a Gem: Bane

But how do we know the whole thing works?

Page 35: Anatomy of a Gem: Bane

TDD Loop

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

Page 36: Anatomy of a Gem: Bane

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

Page 37: Anatomy of a Gem: Bane

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

Page 38: Anatomy of a Gem: Bane

Write Tests in the Language of the Problem Domain

Page 39: Anatomy of a Gem: Bane

This is almost the production code…

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

Page 40: Anatomy of a Gem: Bane

Programmatic Userequire 'bane'include Bane

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

Page 41: Anatomy of a Gem: Bane

# 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

Page 42: Anatomy of a Gem: Bane

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

Page 43: Anatomy of a Gem: Bane

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

Page 44: Anatomy of a Gem: Bane

Close Immediately

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

Page 45: Anatomy of a Gem: Bane

Echo Response

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

Page 46: Anatomy of a Gem: Bane

NeverRespond

class NeverRespond def serve(io) sleep endend

Page 47: Anatomy of a Gem: Bane

NeverRespond

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

Page 48: Anatomy of a Gem: Bane

Photo by Sean T. Allen

Page 49: Anatomy of a Gem: Bane

New Behavior: Server is Not Listening

Page 50: Anatomy of a Gem: Bane

Socket Lifecycle

1. create

2. bind

3. listen

4. accept

5. close

Page 51: Anatomy of a Gem: Bane

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

Page 52: Anatomy of a Gem: Bane

How do we fit this into our GServer-based

code?

Page 53: Anatomy of a Gem: Bane

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!

Page 54: Anatomy of a Gem: Bane

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

Page 55: Anatomy of a Gem: Bane

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?

Page 56: Anatomy of a Gem: Bane

Two Groups to Name• NeverRespond

• CloseImmediately

• FixedResponse

• EchoResponse

• RandomResponse

• …

• NeverListen

• FullListenQueue

• BehaviorServer

Page 57: Anatomy of a Gem: Bane

http://github.com/danielwellman/bane

Bane

[email protected]: @wellman