From: Jim Weirich Date: 2003-01-17T15:27:22+09:00 Subject: Re: Unit Testing in dynamic environments On Thu, 2003-01-16 at 12:22, Travis Whitton wrote: > I'm just starting to get into unit testing, [...] > > Would there be a way to use unit test my get_pct_solids method, or is it only > suited for environments where functions return predictable values? The key is to provide predictable results for the unit test environment. That can be done with a set of test data loaded by "setup", or by mocking the database. Here's a quick sketch of how I might approach it. (Warning: I probably misinterpreted some of your intensions in the code. But I'm probably close enough for this example.) First, start with a Mockup of the database. Here's a simple version ... class MockDb attr_reader :sql, :args def initialize(result) @result = result end def select_one(sql, *args) @sql = sql @args = args @result end end Calling 'select_one' will return the result that was passed to the mock database at creation. Now a test. It looks like you want to return false when there is no data found ... def test_no_results lims = DB.new(MockDb.new(nil)) assert_equal false, lims.get_pct_solids('000001') end The this forces me to write ... class DB def initialize(db) @db = db end def get_pct_solids(sample_number) false end Hmmm ... this test didn't even force us to use the mock database object. The next test will do more. If the data base returns a result, then the answer should be divided by 100.0. def test_one_result answer = { 'sa_result' => 230.0 } lims = DB.new(MockDb.new(answer)) assert_equal 2.3, lims.get_pct_solids('000001') end Now my function looks like ... ... def get_pct_solids(sample_number) row = @db.select_one("SQL???", sample_number) return false if row.nil? row['sa_result'] / 100.0 end ... This gets the process write. Now a simple test on the sql will get that... def test_sql db = MockDb.new(nil) lims = DB.new(db) lims.get_pct_solids('000001') assert_match /SELECT sa_result/, db.sql # More assertions about the select statement as needed. end And the final version of the code looks like ... def get_pct_solids(sample_number) sql = %{ SELECT sa_result FROM sample WHERE sa_sampno = ? AND sa_anaabb LIKE '%SOLIDS' AND (sa_status = 'E' OR SA_STATUS = 'F') } row = @db.select_one(sql, sample_number) return false if row.nil? row['sa_result'] / 100.0 end A couple of thoughts ... Moving the creation of the DBI database connection outside the class makes it easy to pass in mock DBI connections. Plus it makes it easier to use different databases in real life. If you really want to create the connection inside, then provide a way of bypassing that for testing. For example, your initialize method could look like this ... def initialize(db=nil) @db = db || DBI.connect("DBI:xxxxxxxx", "user", "pw") end Also, using select_one makes mocking the database easier ... only one method to mimic. And you don't have to worry about those pesky 'finish' statements (which should probably be in an 'ensure' clause BTW). Of course, if you are repeatably executing the same sql statement over and over, then using prepare/execute is a better choice). If you use an explicit DBI statement object, then you have to provide a MockDb that returns a MockDbiStatement, which can quickly get more complicated. I didn't use the MockObject library. I go back and forth on this. Sometimes I just handcode a quick mock object that does only what I need (besides, I wasn't sure the mock object library works with Test::Unit). Hope this helps. -- -- Jim Weirich jweirich@one.net http://w3.one.net/~jweirich --------------------------------------------------------------------- "Beware of bugs in the above code; I have only proved it correct, not tried it." -- Donald Knuth (in a memo to Peter van Emde Boas)