1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
|
# A brief introduction to testing in Puppet
Puppet relies heavily on automated testing to ensure that Puppet behaves as
expected and that new features don't interfere with existing behavior. There are
three primary sets of tests that Puppet uses: _unit tests_, _integration tests_,
and _acceptance tests_.
- - -
Unit tests are used to test the individual components of Puppet to ensure that
they function as expected in isolation. Unit tests are designed to hide the
actual system implementations and provide canned information so that only the
intended behavior is tested, rather than the targeted code and everything else
connected to it. Unit tests should never affect the state of the system that's
running the test.
- - -
Integration tests serve to test different units of code together to ensure that
they interact correctly. While individual methods might perform correctly, when
used with the rest of the system they might fail, so integration tests are a
higher level version of unit tests that serve to check the behavior of
individual subsystems.
All of the unit and integration tests for Puppet are kept in the spec/ directory.
- - -
Acceptance tests are used to test high level behaviors of Puppet that deal with
a number of concerns and aren't easily tested with normal unit tests. Acceptance
tests function by changing system state and checking the system after
the fact to make sure that the intended behavior occurred. Because of this
acceptance tests can be destructive, so the systems being tested should be
throwaway systems.
All of the acceptance tests for Puppet are kept in the acceptance/tests/
directory. Running the acceptance tests is much more involved than running the
spec tests. Information about how to run them can be found in the [acceptance
testing documentation](https://github.com/puppetlabs/puppet/blob/master/acceptance/README.md).
## Testing dependency version requirements
Puppet is only compatible with a specific version of RSpec. If you are not
using Bundler to install the required test libraries you must ensure that you
are using the right library version. Using an unsupported version of RSpec will
probably display many spurious failures. The supported version of RSpec can be
found in the project Gemfile.
## Puppet Continuous integration
* GitHub Actions (spec tests only): https://github.com/puppetlabs/puppet/actions
* Jenkins (spec and acceptance tests): https://jenkins.puppetlabs.com/view/Puppet%20FOSS/
## RSpec
Puppet uses RSpec to perform unit and integration tests. RSpec handles a number
of concerns to make testing easier:
* Executing examples and ensuring the actual behavior matches the expected behavior (examples)
* Grouping tests (describe and contexts)
* Setting up test environments and cleaning up afterwards (before and after blocks)
* Isolating tests (mocks and stubs)
#### Examples and expectations
At the most basic level, RSpec provides a framework for executing tests (which
are called examples) and ensuring that the actual behavior matches the expected
behavior (which are done with expectations)
```ruby
# This is an example; it sets the test name and defines the test to run
specify "one equals one" do
# add an expectation that left and right arguments are equal
expect(1).to eq(1)
end
# Examples can be declared with either 'it' or 'specify'
it "one doesn't equal two" do
expect(1).to_not eq(2)
end
```
Good examples generally do as little setup as possible and only test one or two
things; it makes tests easier to understand and easier to debug.
More complete documentation on expectations is available at https://www.relishapp.com/rspec/rspec-expectations/docs
Note Puppet supports the [RSpec 3](http://rspec.info/blog/2013/07/the-plan-for-rspec-3/)
API, so please do not use RSpec 2 "should" syntax like `1.should == 1`.
### Example groups
Example groups are fairly self explanatory; they group similar examples into a
set.
```ruby
describe "the number one" do
it "is larger than zero" do
expect(1).to be > 0
end
it "is an odd number" do
expect(1).to be_odd # calls 1.odd?
end
it "is not nil" do
expect(1).to be
end
end
```
Example groups have a number of uses that we'll get into later, but one of the
simplest demonstrations of what they do is how they help to format
documentation:
```
rspec ex.rb --format documentation
the number one
is larger than zero
is an odd number
is not nil
Finished in 0.00516 seconds
3 examples, 0 failures
```
### Setting up and tearing down tests
Examples may require some setup before they can run, and might need to clean up
afterwards. `before` and `after` blocks can be used before this, and can be
used inside of example groups to limit how many examples they affect.
```ruby
describe "something that could warn" do
before :each do
# Disable warnings for this test
$VERBOSE = nil
end
after :each do
# Enable warnings afterwards
$VERBOSE = true
end
it "doesn't generate a warning" do
MY_CONSTANT = 1
# reassigning a constant normally prints out 'warning: already initialized constant FOO'
MY_CONSTANT = 2
end
end
```
### Setting up helper data
Some examples may require setting up data before hand and making it available to
tests. RSpec provides helper methods with the `let` method call that can be used
inside of tests.
```ruby
describe "a helper object" do
# This creates an array with three elements that we can retrieve in tests. A
# new copy will be made for each test.
let(:my_helper) do
['foo', 'bar', 'baz']
end
it "is an array" do
expect(my_helper).to be_a_kind_of Array
end
it "has three elements" do
expect(my_helper.size).to eq(3)
end
end
```
Like `before` blocks, helper objects like this are used to avoid doing a lot of
setup in individual examples and share setup between similar tests.
### Isolating tests with stubs
RSpec allows you to provide fake data during testing to make sure that
individual tests are only running the code being tested. You can stub out entire
objects, or just stub out individual methods on an object. When a method is
stubbed the method itself will never be called.
The RSpec Mocks documentation can be found at https://relishapp.com/rspec/rspec-mocks/v/3-8/docs/
```ruby
describe "stubbing a method on an object" do
let(:my_helper) do
['foo', 'bar', 'baz']
end
it 'has three items before being stubbed' do
expect(my_helper.size).to eq(3)
end
describe 'when stubbing the size' do
before :each do
allow(my_helper).to receive(:size).and_return(10)
end
it 'has the stubbed value for size' do
expect(my_helper.size).to eq(10)
end
end
end
```
Entire objects can be stubbed as well.
```ruby
describe "stubbing an object" do
let(:my_helper) do
double(:not_an_array, :size => 10)
end
it 'has the stubbed size'
expect(my_helper.size).to eq(10)
end
end
```
### Adding expectations with mocks
It's possible to combine the concepts of stubbing and expectations so that a
method has to be called for the test to pass (like an expectation), and can
return a fixed value (like a stub).
```ruby
describe "mocking a method on an object" do
let(:my_helper) do
['foo', 'bar', 'baz']
end
describe "when mocking the size" do
before :each do
expect(my_helper).to receive(:size).and_return(10)
end
it "adds an expectation that a method was called" do
my_helper.size
end
end
end
```
Like stubs, entire objects can be mocked.
```ruby
describe "mocking an object" do
let(:my_helper) do
double(:not_an_array)
end
before :each do
expect(not_an_array).to receive(:size).and_return(10)
end
it "adds an expectation that the method was called" do
not_an_array.size
end
end
```
### Writing tests without side effects
When properly written each test should be able to run in isolation, and tests
should be able to be run in any order. This makes tests more reliable and allows
a single test to be run if only that test is failing, instead of running all
17000+ tests each time something is changed. However, there are a number of ways
that can make tests fail when run in isolation or out of order.
#### Narrowing down a spec test with side effects
If you do have a test that passes in isolation but fails when run as part of
a full spec run, you can often narrow down the culprit by a two-step process.
First, run:
```
bundle exec rake spec
```
which should generate a spec_order.txt file.
Second, run:
```
util/binary_search_specs.rb <full path to failing spec>
```
And it will (usually) tell you the test that makes the failing spec fail.
The 'usually' caveat is because there can be spec failures that require
specific ordering between > 2 spec files, and this tool only handles the
case for 2 spec files. The > 2 case is rare and if you suspect you're in
that boat, there isn't an established best practice, other than to ask
for help on IRC or the mailing list.
#### Using instance variables
Puppet has a number of older tests that use `before` blocks and instance
variables to set up fixture data, instead of `let` blocks. These can retain
state between tests, which can lead to test failures when tests are run out of
order.
```ruby
# test.rb
RSpec.configure do |c|
c.mock_with :rspec
end
describe "fixture data" do
describe "using instance variables" do
# BAD
before :all do
# This fixture will be created only once and will retain the `foo` stub
# between tests.
@fixture = double('test data')
end
it "can be stubbed" do
allow(@fixture).to receive(:foo).and_return(:bar)
expect(@fixture.foo).to eq(:bar)
end
it "does not keep state between tests" do
# The foo stub was added in the previous test and shouldn't be present
# in this test.
expect { @fixture.foo }.to raise_error
end
end
describe "using `let` blocks" do
# GOOD
# This will be recreated between tests so that state isn't retained.
let(:fixture) { double('test data') }
it "can be stubbed" do
allow(fixture).to receive(:foo).and_return(:bar)
expect(fixture.foo).to eq(:bar)
end
it "does not keep state between tests" do
# since let blocks are regenerated between tests, the foo stub added in
# the previous test will not be present here.
expect { fixture.foo }.to raise_error
end
end
end
```
```
bundle exec rspec test.rb -fd
fixture data
using instance variables
can be stubbed
should not keep state between tests (FAILED - 1)
using `let` blocks
can be stubbed
should not keep state between tests
Failures:
1) fixture data using instance variables should not keep state between tests
Failure/Error: expect { @fixture.foo }.to raise_error
expected Exception but nothing was raised
# ./test.rb:17:in `block (3 levels) in <top (required)>'
Finished in 0.00248 seconds
4 examples, 1 failure
Failed examples:
rspec ./test.rb:16 # fixture data using instance variables should not keep state between tests
```
### RSpec references
* RSpec core docs: https://www.relishapp.com/rspec/rspec-core/docs
* RSpec guidelines with Ruby: http://betterspecs.org/
|