You are on page 1of 135

Presentation at

http://bit.ly/codemash-testing

Friday, January 14, 2011 1


Are You Satisfied
with Your Tests?
-- or --
Why don't we do it like this ...

Jim Weirich
Chief Scientist / EdgeCase
jim@edgecase.com
@jimweirich

Friday, January 14, 2011 2


Testing

Friday, January 14, 2011 3


Your Doing it
All Wrong!!!!

Friday, January 14, 2011 3


Survey ...

Friday, January 14, 2011 4


Unit Tests?
Java VS .Net VS Functional?
Python VS Ruby Javascript?
Developers? End to end?

Testing:
TDD/BDD?
Unit Testing?
Any Testing?

Friday, January 14, 2011 5


Unit Tests?
Java VS .Net VS Functional?
Python VS Ruby Javascript?
Developers? End to end?

Are you happy


with your testing?
Testing:
TDD/BDD?
Unit Testing?
Any Testing?

Friday, January 14, 2011 5


Slow Tests

Friday, January 14, 2011 6


Jeff Nielsen
Psychology of Build Times

•Unit Tests

•Checkin Tests

Friday, January 14, 2011 7


Jeff Nielsen
Psychology of Build Times

•Unit Tests <10 seconds

•Checkin Tests

Friday, January 14, 2011 7


Jeff Nielsen
Psychology of Build Times

•Unit Tests <10 seconds

•Checkin Tests <10 Minutes

Friday, January 14, 2011 7


Blasé Attitude
toward Failing Tests

Friday, January 14, 2011 8


Friday, January 14, 2011 9
Fear leads to anger,
anger leads to hate,
hate leads to suffering.
-- Yoda
Friday, January 14, 2011 10
Failing tests lead to anger,
anger leads to hate,
hate leads to suffering.
-- Yoda
Friday, January 14, 2011 11
Friday, January 14, 2011 12
Over Mocking

Friday, January 14, 2011 13


When to Mock

• Using an external service


• Verifying a protocol
• Objects are complicated to create

Friday, January 14, 2011 14


When to Mock

• Using an external service


• Verifying a protocol
• Objects are complicated to create

Friday, January 14, 2011 14


External Service

Your Auth Client Auth


Application Lib Service

Friday, January 14, 2011 15


External Service

Your
Application
X
Auth Client
Lib
Auth
Service

Mocked
Authorization
Client Lib

Friday, January 14, 2011 16


Verifying a Protocol
Client
Software

get_connection
(many times)

Connction Pool
get_connection
release_connection

get_connection
(once)

DB
get_connection
release_connection

Friday, January 14, 2011 17


Verifying a Protocol
Client
Software

get_connection
(many times)

Connction Pool
get_connection
release_connection

get_connection
(once)

X
Mock DB
get_connection get_connection
release_connection release_connection

Friday, January 14, 2011 18


Verifying a Protocol
Client
Software

get_connection
(many times)
Code Under Test
Connction Pool
get_connection
release_connection

get_connection
(once)

X
Mock DB
get_connection get_connection
release_connection release_connection

Friday, January 14, 2011 19


Verifying a Protocol
Client
Software

get_connection
(many times)
Code Under Test
Connction Pool
get_connection
release_connection
Mock
get_connection
(once)

X
Mock DB
get_connection get_connection
release_connection release_connection

Friday, January 14, 2011 19


Overmocking Clues

• You create mocks returning mocks


• You have fantasy tests

Friday, January 14, 2011 20


price
Service Trip
Contract
discount
price
amount_charged * 1

Friday, January 14, 2011 21


test "applies discounts to service price" do
contract = flexmock(:model, Contract,
:price => 100.0)
trip = Factory.build(:service_trip,
:discount => 0.30,
:contract => contract)

assert_equal 70.00, trip.amount_charged


end

Friday, January 14, 2011 22


Mocked to return value

test "applies discounts to service price" do


contract = flexmock(:model, Contract,
:price => 100.0)
trip = Factory.build(:service_trip,
:discount => 0.30,
:contract => contract)

assert_equal 70.00, trip.amount_charged


end

Friday, January 14, 2011 22


Refactor!

price
Service Trip
Contract
discount
price
amount_charged * 1

Friday, January 14, 2011 23


Refactor!

price
Service Trip
Contract
discount
price_per_visit
amount_charged * 1

Friday, January 14, 2011 24


Refactor!

price
Service Trip
Contract
discount
price_per_visit
amount_charged * 1

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Friday, January 14, 2011 24


Refactor!
Wrong method name!

price
Service Trip
Contract
discount
price_per_visit
amount_charged * 1

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Friday, January 14, 2011 24


Fix Service Trip
Correct Method Name

price_per_visit
Service Trip
Contract
discount
price_per_visit
amount_charged * 1

Friday, January 14, 2011 25


Fix Service Trip
Correct Method Name

price_per_visit
Service Trip
Contract
discount
price_per_visit
amount_charged * 1

NoMethodError: undefined method `price_per_visit'


for "#<FlexMock>":Service
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

Friday, January 14, 2011 25


Fantasy Tests

• Pass when the code is incorrect.


• Fail when the code is correct.

Friday, January 14, 2011 26


Summary

• Use a mock to
• Mock an external service
• Verify a protocol
• Don't use a mock to:
• Avoid creating complex Objects

Friday, January 14, 2011 27


Complex
Object Builds

Friday, January 14, 2011 28


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 29


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 29


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 29


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 29


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 29


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 29


Customer Service Type
Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 30


Factory.define :service_trip do |trip|
trip.association :service_tech
trip.association :missed_service_reason
trip.association :contract
trip.discount 0.0
end

Friday, January 14, 2011 31


Create in Database

test "Time for Factory.create" do


bench("Factory.create") {
TIMES.times do
trip = Factory.create(:service_trip)
end
}
end

Friday, January 14, 2011 32


Create in Database

test "Time for Factory.create" do


bench("Factory.create") {
TIMES.times do
trip = Factory.create(:service_trip)
end
}
end

Time: 4.75 Seconds

Friday, January 14, 2011 32


Faster Database
(sqlite3)
test "Time for Factory.create" do
bench("Factory.create") {
TIMES.times do
trip = Factory.create(:service_trip)
end
}
end

Friday, January 14, 2011 33


Faster Database
(sqlite3)
test "Time for Factory.create" do
bench("Factory.create") {
TIMES.times do
trip = Factory.create(:service_trip)
end
}
end

Time: 4.37 Seconds

Friday, January 14, 2011 33


Factory In-Memory
test "Time for Factory.create" do
bench("Factory.create") {
TIMES.times do
trip = Factory.build(:service_trip)
end
}
end

Friday, January 14, 2011 34


Factory In-Memory
test "Time for Factory.create" do
bench("Factory.create") {
TIMES.times do
trip = Factory.build(:service_trip)
end
}
end Build In Memory

Friday, January 14, 2011 34


Factory In-Memory
test "Time for Factory.create" do
bench("Factory.create") {
TIMES.times do
trip = Factory.build(:service_trip)
end
}
end Build In Memory

Time: 3.98 Seconds

Friday, January 14, 2011 34


Built In Memory

Customer Service Type


Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 35


Built In Database

Customer Service Type


Service Tech
1 1
1
*
* *
* * 1
Service Trip Contract Sales Rep
1
* 1
1
1
Missed 1
* Schedule
Service Schedule
Template
Reason

Friday, January 14, 2011 36


Using Mocks
test "Time for Mocking" do
bench("Flexmock") {
TIMES.times do
trip = Factory.build(:service_trip,
:missed_service_reason =>
flexmock(:model, MissedServiceReason),
:service_tech =>
flexmock(:model, ServiceTech),
:contract => flexmock(:model, Contract))
end
}
end

Friday, January 14, 2011 37


Using Mocks
test "Time for Mocking" do
bench("Flexmock") {
TIMES.times do
trip = Factory.build(:service_trip,
:missed_service_reason =>
flexmock(:model, MissedServiceReason),
:service_tech =>
flexmock(:model, ServiceTech),
:contract => flexmock(:model, Contract))
end
}
end

Time: 0.59 seconds

Friday, January 14, 2011 37


Using Factory.attributes_for
test "Time for Custom Factory" do
bench("Factory attributes") {
TIMES.times do
attrs = Factory.attributes_for(:service_trip)
attrs.merge(
:missed_service_reason => ...,
:service_tech => ...,
:contract => ...)
trip = ServiceTrip.new(attrs)
end
}
end

Friday, January 14, 2011 38


Using Factory.attributes_for
test "Time for Custom Factory" do
bench("Factory attributes") {
TIMES.times do
attrs = Factory.attributes_for(:service_trip)
attrs.merge(
:missed_service_reason => ...,
:service_tech => ...,
:contract => ...)
trip = ServiceTrip.new(attrs)
end
}
end

Time: 0.30 seconds

Friday, January 14, 2011 38


Custom In-Memory
test "Time for Custom Build" do
bench("Custom") {
TIMES.times do
trip = ServiceTrip.new(
:missed_service_reason =>
MissedServiceReason.new,
:service_tech => ServiceTech.new,
:contract => Contract.new)
end
}
end

Friday, January 14, 2011 39


Custom In-Memory
test "Time for Custom Build" do
bench("Custom") {
TIMES.times do
trip = ServiceTrip.new(
:missed_service_reason =>
MissedServiceReason.new,
:service_tech => ServiceTech.new,
:contract => Contract.new)
end
}
end

Custom: 0.06 Seconds

Friday, January 14, 2011 39


Timing Summary
Factory / mysql

Factory / sqlite3

Factory Build

Mocks d

Factory / Attrs

Raw New

0 1.25 2.50 3.75 5.00

Seconds

Friday, January 14, 2011 40


Gratuitous Use
of the Database

Friday, January 14, 2011 41


def test_total_cost
order = Order.create(
:items => [Item.create(:cost => 10)])
assert_equal 10, order.total_cost
end

Friday, January 14, 2011 42


In the Database?

def test_total_cost
order = Order.create(
:items => [Item.create(:cost => 10)])
assert_equal 10, order.total_cost
end

Friday, January 14, 2011 42


def test_total_cost
order = Order.new(
Text
:items => [Item.new(:cost => 10)])
assert_equal 10, order.total_cost
end

Friday, January 14, 2011 43


save VS valid?

def test_order_fails_with_bad_supplier
order = Order.new(:supplier => :bad)
assert ! order.save
end

Friday, January 14, 2011 44


save VS valid?

def test_order_fails_with_bad_supplier
order = Order.new(:supplier => :bad)
assert ! order.valid?
end

Friday, January 14, 2011 45


save VS valid?
def test_order_fails_with_bad_supplier
order = Order.new(:supplier => :bad)

assert ! order.valid?
assert model.errors.on(:supplier)
assert_match(/(invalid|bad).*supplier/i,
model.errors.on(field).to_s,
end

Friday, January 14, 2011 46


Custom Assertions

Friday, January 14, 2011 47


def assert_tween(min, max, actual, name)
assert actual >= min,
"#{name} must be >= #{min} (was #{actual})"
Text
assert actual <= max,
"#{name} must be <= #{max} (was #{actual})"
end

Friday, January 14, 2011 48


should 'be randomly distributed' do
collect_face_counts.each do |face, count|
assert_tween 1, 6, face, "face"
Text
assert_tween 800, 1200, count, "count"
end
end

Friday, January 14, 2011 49


def assert_validation_error_on(model,
field=nil,
pattern=//)
if field
assert ! model.valid?
assert model.errors.on(field)
assert_match(re,
model.errors.on(field).to_s)
else
assert ! model.valid?
assert_match(re,
model.errors.full_messages.join(", "))
end
end

(note: real version has custom error messages)


Friday, January 14, 2011 50
Over Meta in Tests

Friday, January 14, 2011 51


%w(name address).each do |feature|
it "clears the #{feature} field" do
@item.send("clear_#{feature}")
Text
assert_equal "", @item.name
end
end

Friday, January 14, 2011 52


Explicit Reference

%w(name address).each do |feature|


it "clears the #{feature} field" do
@item.send("clear_#{feature}")
Text
assert_equal "", @item.name
end
end

Friday, January 14, 2011 52


it "clears the name field" do
@item.clear_name
assert_equal "", @item.name
end
Text
it "clears the address field" do
@item.clear_address
assert_equal "", @item.address
end

Friday, January 14, 2011 53


Testing Private Methods

Friday, January 14, 2011 54


describe :load_personal_data do
before do
@entry = stubbed_entry
@entry.stub!(:owner).and_return(:owner_id)
controller.instance_variable_set('@entry', @entry)
end

it "loads the personal data from from the" do


Text
controller.stub!(:params).
and_return(:person => 'John Doe')
PersonalDataService.
should_receive(:get_personal_data).
with(:owner_id)

controller.send(:load_personal_data)
end
end

Friday, January 14, 2011 55


describe :load_personal_data do
before do
@entry = stubbed_entry
@entry.stub!(:owner).and_return(:owner_id)
controller.instance_variable_set('@entry', @entry)
end

Pathelogical
it "loads the personal data Coupling
from from the" do
Text
controller.stub!(:params).
and_return(:person => 'John Doe')
PersonalDataService.
should_receive(:get_personal_data).
with(:owner_id)

controller.send(:load_personal_data)
end
end

Friday, January 14, 2011 56


describe :load_personal_data do
before do
@entry = stubbed_entry
@entry.stub!(:owner).and_return(:owner_id)
controller.instance_variable_set('@entry', @entry)
end

it "loads the personal data from from the" do


Text
controller.stub!(:params).
and_return(:person => 'John Doe')
PersonalDataService.
More Pathelogical Coupling
should_receive(:get_personal_data).
with(:owner_id)

controller.send(:load_personal_data)
end
end

Friday, January 14, 2011 57


describe :load_personal_data do
before do
@entry = stubbed_entry
@entry.stub!(:owner).and_return(:owner_id)
controller.instance_variable_set('@entry', @entry)
end

it "loads the personal data from from the" do


Text
controller.stub!(:params).
and_return(:person => 'John Doe')
PersonalDataService.
Bypass Normal Privacy Controls
should_receive(:get_personal_data).
with(:owner_id)

controller.send(:load_personal_data)
end
end

Friday, January 14, 2011 58


In Campfire ...

Jim W: Move it to another class and test that class


Testing private controller methods via send
makes controller tests WAY too brittle.
d
Scott B: it also kills unicorns
so - congratulations. now they're extinct.

Friday, January 14, 2011 59


Friday, January 14, 2011 60
Friday, January 14, 2011 61
Friday, January 14, 2011 62
Incorrect use of
Describe / Context

Friday, January 14, 2011 63


it "clears the name field" do
@item.clear_name
assert_equal "", @item.name
end
Text
it "clears the address field" do
@item.clear_address
assert_equal "", @item.address
end

Friday, January 14, 2011 64


describe Item do
describe "#clear_name" do
it "clears the name field" do
...
end
end
Text
describe "#clear_address" do
it "clears the address field" do
...
end
end
end

Friday, January 14, 2011 65


describe "#score" do
before { @bowling = BowlingScorer.new }
context "with no throws" do
before { @throws = [] }
it "returns zero" do
@bowling.score(@throws).should == 0
end
end Text
context "with one throw" do
before { @throws = [9] }
it "returns the throw" do
@bowling.score(@throws).should == 9
end
end
end

Friday, January 14, 2011 66


describe "#score" do
before { @bowling = BowlingScorer.new }
context "with no throws" do
before { @throws = [] }
it "returns zero" do
@bowling.score(@throws).should == 0
end
end Text
context "with one throw" do
before { @throws = [9] }
it "returns the throw" do
@bowling.score(@throws).should == 9
end
end
end

Friday, January 14, 2011 67


describe "#score" do
before { @bowling = BowlingScorer.new }
context "with no throws" do
before { @throws = [] }
it "returns zero" do
@bowling.score(@throws).should == 0
end
end Text
context "with one throw" do
before { @throws = [9] }
it "returns the throw" do
@bowling.score(@throws).should == 9
end
end
end

Friday, January 14, 2011 68


describe "#score" do
before { @bowling = BowlingScorer.new }
context "with no throws" do
before { @throws = [] }
it "returns zero" do
@bowling.score(@throws).should == 0
end
end Text
context "with one throw" do
before { @throws = [9] }
it "returns the throw" do
@bowling.score(@throws).should == 9
end
end
end

Friday, January 14, 2011 69


Guidelines

• Use describe with ...


• Things
• (class names, method names)
• Use context with ...
• Situations
• (when ..., with ...)

Friday, January 14, 2011 70


Refactoring Tests

Friday, January 14, 2011 71


describe "#score" do
before { @bowling = BowlingScorer.new }
context "with no throws" do
before { @throws = [] }
it "returns zero" do
@bowling.score(@throws).should == 0
end
end Text
context "with one throw" do
before { @throws = [9] }
it "returns the throw" do
@bowling.score(@throws).should == 9
end
end
end

Friday, January 14, 2011 72


describe "#score" do
let(:bowling) { BowlingScorer.new }
context "with no throws" do
let(:throws) { [] }
it "returns zero" do
bowling.score(throws).should == 0
end
end Text
context "with one throw" do
let(:throws) { [9] }
it "returns the throw" do
bowling.score(throws).should == 9
end
end
end

Friday, January 14, 2011 73


describe "#score" do
let(:bowling) { BowlingScorer.new }
context "with no throws" do
let(:throws) { [] }
it "returns zero" do
bowling.score(throws).should == 0
end
Lazy Initialization
end Text
context "with one throw" do
let(:throws) { [9] }
it "returns the throw" do
bowling.score(throws).should == 9
end
end
end

Friday, January 14, 2011 74


describe "#score" do
let(:bowling) { BowlingScorer.new }
context "with no throws" do
let(:throws) { [] }
it "returns zero" do
bowling.score(throws).should == 0
end
Lazy Initialization
end Text
context "with one throw" do
let(:throws) { [9] }
it "returns the throw" do
bowling.score(throws).should == 9
end
end
end

Friday, January 14, 2011 75


describe "#score" do
let(:bowling) { BowlingScorer.new }
context "with no throws" do
let(:throws) { [] }
it "returns zero" do
bowling.score(throws).should == 0
end
end Text
Using Lazy Initializations
context "with one throw" do
let(:throws) { [9] }
it "returns the throw" do
bowling.score(throws).should == 9
end
end
end

Friday, January 14, 2011 76


it "should return the throw" do
bowling.score(throws).should
Text == 9
end

Friday, January 14, 2011 77


it "should return the throw" do
bowling.score(throws).should
Text == 9
end

Friday, January 14, 2011 77


it "returns the throw" do
bowling.score(throws).should
Text == 9
end

Friday, January 14, 2011 78


describe Stack do
context "stack with one item" do
let(:stack) { a_stack_with_one_item }
context "when popped" do
before { stack.pop }
it "is empty" Text
do
stack.should be_empty
end
end
end
end

Friday, January 14, 2011 79


describe Stack do
context "stack with one item" do
let(:stack) { a_stack_with_one_item }
context "when popped" do
before { stack.pop }
it "is empty" Text
do
stack.should be_empty
end
end
end
end

Friday, January 14, 2011 79


describe Stack do
context "stack with one item" do
let(:stack) { a_stack_with_one_item }
context "when popped" do
before { stack.pop }
it "is empty" Text
do
stack.should be_empty
end
end
end
end

Friday, January 14, 2011 79


describe Stack do
context "stack with one item" do
let(:stack) { a_stack_with_one_item }
context "when popped" do
before { stack.pop }
it "is empty" Text
do
stack.should be_empty
end
end
end
end

Friday, January 14, 2011 79


describe Stack do
context "nearly empty" do
subject { a_stack_with_one_item }
context "when popped" do
Text
before { subject.pop }
it { should be_empty }
end
end
end

Friday, January 14, 2011 80


Declare Subject
describe Stack do
context "nearly empty" do
subject { a_stack_with_one_item }
context "when popped" do
Text
before { subject.pop }
it { should be_empty }
end
end
end

Friday, January 14, 2011 81


Explicit Use
describe Stack do
context "nearly empty" do
subject { a_stack_with_one_item }
context "when popped" do
Text
before { subject.pop }
it { should be_empty }
end
end
end

Friday, January 14, 2011 82


describe Stack do
context "nearly empty" do
subject { a_stack_with_one_item }
context "when popped" do
Text
before { subject.pop }
it { should be_empty }
end
end Implicit Use
end

Friday, January 14, 2011 83


What Code is Under Test?

describe Stack do
context "nearly empty" do
Text
subject { a_stack_with_one_item }
context "when popped" do
before { subject.pop }
it { should be_empty }
end
end
end

Friday, January 14, 2011 84


What Code is Under Test?
What Code is Setup?
describe Stack do
context "nearly empty" do
Text
subject { a_stack_with_one_item }
context "when popped" do
before { subject.pop }
it { should be_empty }
end
end
end

Friday, January 14, 2011 85


gem rspec-given

describe Stack do
context "nearly empty" do
Given(:stack) { a_stack_with_one_item }
When { stack.pop Text
}
Then { stack.should be_empty }
end
end

Friday, January 14, 2011 86


Specifications
(not tests)

Friday, January 14, 2011 87


TDD BDD

Friday, January 14, 2011 88


Testing Specifying
Code Behavior

Friday, January 14, 2011 89


RSpec != Specifying
Behavior

Friday, January 14, 2011 90


Two Questions

Friday, January 14, 2011 91


(1)
If I wanted to use this
software in my project,
what behaviors are
important to me?

Friday, January 14, 2011 92


(1)

Necessary

Friday, January 14, 2011 93


(2)
Could I write this
software from scratch
using only the tests/
specs as guidance?

Friday, January 14, 2011 94


(2)

Sufficient

Friday, January 14, 2011 95


Things that are
Important ...

Friday, January 14, 2011 96


public methods
(names, args, contract)

Friday, January 14, 2011 97


public protocols

Friday, January 14, 2011 98


Things that are
NOT Important ...

Friday, January 14, 2011 99


private methods

Friday, January 14, 2011 100


Ancillary Objects

Friday, January 14, 2011 101


Summary

Friday, January 14, 2011 102


(1) Tests are Code
Treat them with the same respect
as the rest of your source code.

Friday, January 14, 2011 103


(2) Tests are
Specifications
Focus on the What,
Not the How

Friday, January 14, 2011 104


Questions?

Jim Weirich
Chief Scientist / EdgeCase
jim@edgecase.com
@jimweirich

(photo by James Duncan Davidson)


Friday, January 14, 2011 105
Image Attributions
cables: Scott the (angrykeyboarder on Flickr)
snail: http://photozou.jp/photo/show/38290/21923871
data center: Christopher Bowns (cbowns on Flickr)
report card: (amboo who? on Flickr)
giraffe: (Kurt Thomas Hunt on Flickr)
custom guitar: (The Creamery on Picasa)
escher mirror: (Bert K on Flickr)
privacy: Rob Pongsajapan (rpongsaj on Flickr)
russian dolls: (frangipani photograph on Flickr)
shuttle specs: Tom Peck (ThreadedThoughts on Flickr)
bubble wrap: GNU Free Documentation License.
questions: (Rock Alien on Flickr)

Friday, January 14, 2011 106


License

Friday, January 14, 2011 107

You might also like