Quantcast
Channel: Specify – Stories from a Software Tester
Viewing all articles
Browse latest Browse all 2

Specify Use Case: Models, Rules and Features

$
0
0

I introduced my Specify micro-framework in a previous post. In this post I want to cover an example of how effective I think this kind of approach can be.

This post will be short on details and heavy on code. My goal here is to show what I think is an interesting idea about providing a domain model for what you are working on.

Going with the example from my previous post, imagine you have a “planetary weight” application. This may have components like a REST interface as well as a web-based application. But somewhere in your logic you have elements that actually perform the calculations that allow someone’s weight on another planet to be calculated.

What I want Specify to do is serve as an abstraction layer over unit tests and integration tests. One way to do this is to test to a model. That model can specify the business rules (that you might normally unit test) and the features (that you might normally integration test).

So imagine you have a model like this:

class Planet < Domain
  url_is 'http://localhost:9292/weight'
  url_matches /:\d{4}\/weight/
  title_is 'Dialogic - Weight Calculator'

  # Model Execution

  class << self
    G = 6.674e-11
    EARTH_SG = 9.81

    EARTH = { mass: 5.97e24, radius: 6.378e6 }
    MERCURY = { mass: 3.301e23, radius: 2.44e6 }
    VENUS = { mass: 4.8673e24, radius: 6.051e6 }
    MARS = { mass: 6.4169e23, radius: 3.397e6 }
    JUPITER = { mass: 1.8981e27, radius: 7.1492e7 }
    SATURN = { mass: 5.6832e26, radius: 6.0268e7 }
    URANUS = { mass: 8.6810e25, radius: 2.5559e7 }
    NEPTUNE = { mass: 1.0241e26, radius: 2.4764e7 }

    def find_surface_gravity_ratio_for(planet)
      force = G * self.instance_eval("#{planet.upcase}[:mass]")
      distance = self.instance_eval("#{planet.upcase}[:radius]")**2
      equator_sg = force / distance
      ratio = equator_sg / EARTH_SG
    end

    def a_weight_of(weight)
      @weight = weight
      self
    end

    def is_what_on(planet)
      @weight * find_surface_gravity_ratio_for(planet)
    end
  end

  # Page Elements

  text_field :weight,    id: 'wt'
  text_field :mercury,   id: 'outputmrc'
  text_field :venus,     id: 'outputvn'
  text_field :mars,      id: 'outputmars'
  text_field :jupiter,   id: 'outputjp'
  text_field :saturn,    id: 'outputsat'
  text_field :uranus,    id: 'outputur'
  text_field :neptune,   id: 'outputnpt'

  button     :calculate, id: 'calculate'

  # Page Methods

  def convert(value)
    weight.set value
    calculate.click
  end

  def get_weight_for(planet)
    self.send("#{planet}").when_present.value.to_f
  end

  def confirm_weight_for(planet, value)
    weight = self.send("#{planet}".downcase).when_present.value
    expect(weight.to_f).to eq value.to_f
  end

  def confirm_approximate_weight_for(planet, value, threshold)
    weight = self.send("#{planet}".downcase).when_present.value.to_f
    expect(weight.to_f).to be_within(threshold).of(value.to_f.floor)
  end
end

Note the section under the comment Model Execution. That’s not the actual code of the application necessarily. In fact, in my case it’s not: the planetary app I wrote uses JavaScript. But what the above section of code does is provide a model of how that part of the application works. This allows me to test my assumptions about it.

So I could provide a Specify file like this:

require 'spec_helper'

Component 'Planet Weight Calculator' do

  surface_gravity = [
      { planet: 'Earth',    calc: Planet.find_surface_gravity_ratio_for('Earth') },
      { planet: 'Mercury',  calc: Planet.find_surface_gravity_ratio_for('Mercury') },
      { planet: 'Venus',    calc: Planet.find_surface_gravity_ratio_for('Venus') },
      { planet: 'Mars',     calc: Planet.find_surface_gravity_ratio_for('Mars') },
      { planet: 'Jupiter',  calc: Planet.find_surface_gravity_ratio_for('Jupiter') },
      { planet: 'Saturn',   calc: Planet.find_surface_gravity_ratio_for('Saturn') },
      { planet: 'Uranus',   calc: Planet.find_surface_gravity_ratio_for('Uranus') },
      { planet: 'Neptune',  calc: Planet.find_surface_gravity_ratio_for('Neptune') }
  ]

  weight = [
      { planet: 'Earth',    calc: Planet.a_weight_of(200).is_what_on('Earth') },
      { planet: 'Mercury',  calc: Planet.a_weight_of(200).is_what_on('Mercury') },
      { planet: 'Venus',    calc: Planet.a_weight_of(200).is_what_on('Venus') },
      { planet: 'Mars',     calc: Planet.a_weight_of(200).is_what_on('Mars') },
      { planet: 'Jupiter',  calc: Planet.a_weight_of(200).is_what_on('Jupiter') },
      { planet: 'Saturn',   calc: Planet.a_weight_of(200).is_what_on('Saturn') },
      { planet: 'Uranus',   calc: Planet.a_weight_of(200).is_what_on('Uranus') },
      { planet: 'Neptune',  calc: Planet.a_weight_of(200).is_what_on('Neptune') }
  ]

  rules 'Surface Gravity Calculation' do

    Rule 'surface gravity can be calculated for all planets' do
      surface_gravity.each do |calculate|
        specify "#{calculate[:planet]} surface gravity: #{calculate[:calc]}" do; end
      end
    end

    Rule 'weight is calculated based on surface gravity' do
      weight.each do |calculate|
        specify "#{calculate[:planet]} weight: #{calculate[:calc]}" do; end
      end
    end

  end

end

Were you to run this, you get the following output:

Planet Weight Calculator
  Surface Gravity Calculation
    surface gravity can be calculated for all planets
      Earth surface gravity: 0.9984412061578731
      Mercury surface gravity: 0.3772098862532158
      Venus surface gravity: 0.9043801139180913
      Mars surface gravity: 0.3783130934843111
      Jupiter surface gravity: 2.5265121478475008
      Saturn surface gravity: 1.0644777190561834
      Uranus surface gravity: 0.9040641234578886
      Neptune surface gravity: 1.136103689974277
    weight is calculated based on surface gravity
      Earth weight: 199.68824123157464
      Mercury weight: 75.44197725064315
      Venus weight: 180.87602278361825
      Mars weight: 75.66261869686221
      Jupiter weight: 505.30242956950013
      Saturn weight: 212.89554381123668
      Uranus weight: 180.8128246915777
      Neptune weight: 227.22073799485537

Finished in 0.00268 seconds (files took 0.38298 seconds to load)
16 examples, 0 failures

So why would you do this? Note that this is not testing the web application at all. It’s simply allowing a model to be executed. This lets us know if we understand exactly how the model should be working. Further, it let’s us reinforce that view with developers or business folks.

For example, a business expert could look at the method find_surface_gravity_ratio_for in the model representation and determine if I’m executing it correctly.

When I want to test the web application itself, I can provide a Specify file for that as well:

require 'spec_helper'

Feature 'Calculate Planet Weights', :phantomjs do
  Background(:all) do
    on_view(Planet)
  end

  Background(:each) do
    on(Planet).convert(200)
  end

  rocky_planets = [
      { planet: 'mercury', weight: '75.6' },
      { planet: 'venus',   weight: '181.4' },
      { planet: 'mars',    weight: '75.4' },
  ]

  gas_giants = [
      { planet: 'jupiter', weight: '472.8' },
      { planet: 'saturn',  weight: '212.8' },
      { planet: 'uranus',  weight: '177.8' },
      { planet: 'neptune', weight: '225' }
  ]

  rocky_planets.each do |example|
    specify "a 200 pound person will weigh #{example[:weight]} on #{example[:planet].capitalize}." do
      on(Planet).confirm_weight_for(example[:planet], example[:weight])
      on(Planet).confirm_approximate_weight_for(example[:planet], example[:weight], 5.9)
    end
  end

  gas_giants.each do |example|
    specify "a 200 pound person will weigh #{example[:weight]} on #{example[:planet].capitalize}." do
      on(Planet).confirm_weight_for(example[:planet], example[:weight])
      on(Planet).confirm_approximate_weight_for(example[:planet], example[:weight], 5.9)
    end
  end
end

The output from this will be the following:

Calculate Planet Weights
  a 200 pound person will weigh 75.6 on Mercury.
  a 200 pound person will weigh 181.4 on Venus.
  a 200 pound person will weigh 75.4 on Mars.
  a 200 pound person will weigh 472.8 on Jupiter.
  a 200 pound person will weigh 212.8 on Saturn.
  a 200 pound person will weigh 177.8 on Uranus.
  a 200 pound person will weigh 225 on Neptune.

Finished in 12.17 seconds (files took 0.35617 seconds to load)
7 examples, 0 failures

Note that this returning a calculation based on the same idea that my model went through. Yet there are differences. For example, the model reports that a 200 pound person would weigh 227.22 pounds on Neptune, whereas the web application is reporting 225. So what we can do is look at if those differences matter. They may be pointing out nothing more than expected deviations given floating point math or they may be pointing out flaws in our understanding of the model.

What I hope you notice in this is the way Specify lets you word your spec files. You can see that I used the Specify DSL slightly differently each time in order to accommodate what I was talking about.

The idea of building a domain model is important, particularly for application domains that are complex. In this example, my domain model serves double-duty: it serves as a mechanism for me to test underlying calculations of the model and also serves as a page object to allow a web service or page to be interacted with. Further, you can wrap these models in specific test DSLs to make the model, the application, and the tests for both more clear.

This is admittedly an area I’m still exploring with Specify but the idea of using a model along with rules and features seems like a promising area of exploration.


Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images