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