Saturday, December 19, 2009

Uncle Bob's Payroll Case Study: Use Case 1

Today I'm going to begin working through Uncle Bob's "Payroll Case Study" from AGILE SOFTWARE DEVELOPMENT. All of my implementations will be done in Ruby. Pick up your copy of AGILE SOFTWARE DEVELOPMENT and code along with me!

This section begins with the specification of a payroll system. The specs include descriptions of varying types of employees and how they are compensated. Uncle Bob then recommends that the reader "implement the first few use cases test-first." This sounds like a good idea. So I decided to implement the 7 use cases from the book on my own first. Here's the first one:

---------------------------------------------------------
USE CASE 1: ADD NEW EMPLOYEE

A new employee is added by the receipt of an AddEmp transaction. This transaction contains the employee's name, address, and assigned employee number. The transaction has the following three forms:

AddEmp [EmpID] "[name]" "[address]" H [hourly-rate]
AddEmp [EmpID] "[name]" "[address]" S [monthly-salary]
AddEmp [EmpID] "[name]" "[address]" C [monthly-salary] [commission-rate]

The employee record is created with its fields assigned appropriately.

Alternative: An error in the transaction structure
If the transaction structure is inappropriate, it is printed out in an error message, and no action is taken.
---------------------------------------------------------

I recognized (too?) quickly that I would need a database to manage the creating and deleting of employees, so I went with DataMapper. This decision was mostly made because I've never used DataMapper before and I thought this would provide me with an easy introduction.

Here are my files and tests to satisfy Use Case 1. I've provided observations on the crafting of this code below.
records_manager_test.rb
require File.join(File.dirname(__FILE__) ,'/test_helper')
require 'lib/records_manager'

class RecordsManagerTest < Test::Unit::TestCase

def setup
DataMapper.auto_migrate!
@any_id = 100
@any_name = "name"
@any_address = "address"
@new_employee_fields = { :emp_id => @any_id,
:name => @any_name,
:address => @any_address,
:salary => new_salary('monthly')
}
@employee_commission = 3.0
@records_manager = RecordsManager.new
end

def teardown
Employee.all.each { |e| e.destroy }
Salary.all.each { |e| e.destroy }
end

def test_add_employee_creates_employee_record
@records_manager.add_employee(@new_employee_fields)

assert_employee_was_created
assert_equal @any_id, new_employee.emp_id
assert_equal @any_name, new_employee.name
assert_equal @any_address, new_employee.address
end

def test_add_employee_creates_hourly_rate_employee
hourly_salary = new_salary('hourly')
assert_creation_of_employee_with hourly_salary
end

def test_add_employee_creates_monthly_rate_employee
monthly_salary = new_salary('monthly')
assert_creation_of_employee_with monthly_salary
end

def test_add_employee_creates_commission_employee
commission_salary = new_salary('monthly', @employee_commission)
assert_creation_of_employee_with commission_salary
end

def test_add_employee_fails_when_provided_no_emp_id
assert_employee_not_created_with_nil :emp_id
end

def test_add_employee_fails_when_provided_no_name
assert_employee_not_created_with_nil :name
end

def test_add_employee_fails_when_provided_no_ddress
assert_employee_not_created_with_nil :address
end

private
def assert_employee_was_created
assert_equal 1, Employee.count
end

def assert_creation_of_employee_with(salary)
@new_employee_fields[:salary] = salary

@records_manager.add_employee(@new_employee_fields)
assert_equal salary, new_employee.salary
end

def assert_employee_not_created_with_nil(field)
@new_employee_fields[field] = nil
error_message = "must not be blank"
assert_match error_message, @records_manager.add_employee(@new_employee_fields)
end

def new_salary(compensation, commission=nil)
Salary.new :pay_rate => pay_rate(compensation),
:compensation => compensation,
:commission => commission
end

def pay_rate(type)
return 5000 if type == 'monthly'
return 12.50 if type == 'hourly'
end

def new_employee
Employee.first
end

end

records_manager.rb
require 'app/models/salary'
require 'app/models/employee'

class RecordsManager

def add_employee(fields)
new_employee = Employee.new(fields)
return if new_employee.save
error_message_for new_employee
end

private

def error_message_for(employee)
employee.errors.full_messages.to_s
end
end

employee.rb
require 'rubygems'
require 'dm-core'
require 'dm-aggregates'
require 'dm-validations'
require 'app/models/salary'

DataMapper.setup(:default, :adapter => 'mysql',
:username => 'root',
:password => 'even-fish',
:database => 'payroll')
class Employee
include DataMapper::Resource

property :id, Serial
property :emp_id, Integer
property :name, String
property :address, Text

has 1, :salary

validates_present :emp_id, :name, :address
end

salary.rb
require 'rubygems'
require 'dm-core'
require 'dm-aggregates'
require 'dm-types'
require 'app/models/employee'

DataMapper.setup(:default, :adapter => 'mysql',
:username => 'root',
:password => 'even-fish',
:database => 'payroll')
class Salary
include DataMapper::Resource

property :id, Serial
property :pay_rate, Float
property :commission, Float
property :compensation, Enum['monthly','hourly']

belongs_to :employee
end


Thoughts:
  • DataMapper is a heavyweight solution for the payroll system at this point. I cheated a little bit in looking at Use Case 2, which specifies that we should be able to delete employees. Still, it gave me an excuse to play with it and I'm pretty confident in it being a good solution as the software evolves.
  • When I created a Salary model I thought immediately that I would need several types of Salary objects. My first thought was to use STI and I originally built it in for this use case. But then I reverted, eventually deciding that I could postpone this decision until the application gets a bit more complex. For now I'm just dealing with the pain of specifying a type of salary in my test cases.
  • You'll notice that function 'add_employee' doesn't really take four or five arguments. It takes a hash because this seemed like a simpler solution. Now DataMapper handles the setting of fields and my RecordsManager object doesn't have to care what type of salary it's being passed. I'm not sure if that's in keeping with the specifications, but I'm going to leave it like that for now.
  • I have many duplicated require statements in my models. Also, I'm duplicating my database setup. A future iteration should abstract this away and DRY up the models.

Wednesday, September 9, 2009

fooberific

Here is some code:



require 'rubygems'
require 'hpricot'
require 'syntax/convertors/html'

hpricot = Hpricot('generate_primes.rb')
convertor = Syntax::Convertors::HTML.for_syntax "ruby"
hpricot.search('//pre[@class="ruby"]') do |e|
e.inner_html = c.convert(e.inner_text, false)
end
puts hpricot.to_s

Wednesday, June 10, 2009

Being a practicing Ruby developer and avid TDDer, there are two things I like at my side when learning something new: an interactive shell and a testing framework.

JS Test Driver may suit my needs for testing (still working on that one). And lo and behold, an interactive shell does exist for JS: Mozilla's Javascript Shell. Despite not being exceptionally convenient to install (as it requires a full build of SpiderMonkey), the shell looks promising so far, particularly to someone who is still exploring the nuances of javascript.

Javascript, Anyone?

In what is now a couple of years of Ruby programming, I'm a little embarrassed to admit how little I know about javascript. The fact is I've used it extremely little in my current work - that is, until now. My team has inherited a project full of js - tangly, untested, js that inhabits every corner of the application.

So I'm doing what I can to get up to speed. I'm starting way back in the beginning with a Head First Javascript book. Most of the first 100 pages have been relatively useful, if extremely fundamental. These books assume you know almost nothing about programming, but they've still managed to get across some of the principals I've been lacking.

In doing the rudimentary exercises described in the book I've found myself craving some sort of testing framework. Javascript is a real language with real, testable code, and a storied history of nightmarish debugging when things go haywire. So why not test drive this code? Until now I've always believed that Selenium would be the answer to any javascript testing needs. But I'm curious to see what incrementally developing with JS unit tests will get me.

I've heard of JS Test Driver and I've spent some time playing around with the framework. I like what I see so far, but I'm coming up short on documentation or other blogs and helpful information beyond the "Getting Started" stage. I'll be on the lookout for these over the next few days, and in the mean time I'll try to post what I learn so maybe others will benefit.