Fake Ruby Test Classes
This is a post from Luke's old blog; it is saved here statically for historical purposes, as of October 2008
Now that I mostly have providers done, I can start taking advantage of it for testing.
For instance, I now have user testing split into two chunks -- providers and
types. The provider testing verifies that all of the methods actually do what
they're supposed to -- calling shell= actually changes the user's shell, etc.:
def attrtest_shell(user)
old = current?(:shell, user)
newshell = findshell(old)
unless newshell
$stderr.puts "Cannot find alternate shell; skipping shell test"
return
end
assert_nothing_raised {
user.shell = newshell
}
assert_equal(newshell, current?(:shell, user),
"Shell was not changed")
assert_nothing_raised {
user.shell = old
}
assert_equal(old, current?(:shell, user), "Shell was not reverted")
end
(This isn't named test_<something> because a different method detects this
method and calls it dynamically.)
This modifies the actual state on the machine; the current? method is
abstracted a bit so it works with the builtin Etc ruby module and knows
how to look in NetInfo (I can't seem to get it to flush its cache fast enough
to use the Etc library reliably on OS X).
So, I've got one set of tests that know how to verify that when I call
provider.shell = "/bin/sh" it does the right thing, which means I don't
need to test that anywhere else. Thus, when I test my types, I just want to
test that the method gets called correctly, I don't need to test what happens
inside the method.
So, I create a base class for fake providers:
class FakeProvider
attr_accessor :model
class << self
attr_accessor :name, :model
end
# Set up methods to fake things
def self.apimethods(*ary)
@model.validstates.each do |state|
ary << state unless ary.include? state
end
# Make accessor methods for everything
attr_accessor *ary
end
# Our fake providers are always suitable
def self.suitable?
true
end
def initialize(model)
@model = model
end
end
Then we create a sub-class for user type testing:
p = Puppet::Type.type(:user).provide :fake, :parent => TestPuppet::FakeProvider do
@name = :fake
apimethods
def create
@ensure = :present
@model.eachstate do |state|
send(state.name.to_s + "=", state.should)
end
end
def delete
@ensure = :absent
@model.eachstate do |state|
send(state.name.to_s + "=", :absent)
end
end
def exists?
if defined? @ensure and @ensure == :present
true
else
false
end
end
end
FakeUserProvider = p
The apimethods call in this subclass causes accessor methods to be created
for every state on users, e.g., gid, gid=, home, home=, etc.
If this isn't appropriate for your class, you can instead pass in a list of
methods to add (e.g., apimethods :mymethod, :othermethod).
The only other step is to mark this as the default provider in your setup code:
def setup
super
Puppet::Type.type(:user).defaultprovider = FakeUserProvider
end
def teardown
# Reset it, so that the system will instead discover the "correct"
# provider.
Puppet::Type.type(:user).defaultprovider = nil
super
end
Now you can test your type without actually modifying the system at all. In this case, if we start with a missing user, set some data up, and then create the user, our provider should have all of the data stored in instance variables (whereas a normal provider would modify a real user).
This is a simplistic, "just make sure the basics are working" test method:
def test_simpleuser
name = "pptest"
user = nil
assert_nothing_raised {
user = Puppet.type(:user).create(
:name => name,
:comment => "Puppet Testing User",
:gid => Process.gid,
:shell => findshell(),
:home => "/home/%s" % name
)
}
assert(user, "Did not create user")
comp = newcomp("usercomp", user)
trans = assert_events([:user_created], comp, "user")
assert_equal(user.should(:comment), user.provider.comment,
"Comment was not set correctly")
assert_rollback_events(trans, [:user_removed], "user")
assert(! user.provider.exists?, "User did not get deleted")
end
A lot of this uses methods I've written in my test system (e.g.,
assert_events, which verifies specific events are generated and thus
specific work was done), but the important point here is that I can start with
a missing user, "create" the user, then verify that the provider (which,
again, would normally interact with /etc/passwd or netinfo, but in this case
just uses instance variables) has the right data.
Actually, that's not right -- the important point is that I can test all of this without actually modifying the system. Before I created this split between types and providers, I had to run this test as root because it had to modify the system and compare against the system; now I can run this as a normal user because it never touches the system.
Now that's progress. :)