Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Generators and stop conditions are essential in AltWalker & GraphWalker (more info herehere, and here), as they influence how the model will be "walked" and until when.

...

Each model has an internal state with some variables - its context. Besides, and since GraphWalker can transverse multiple models, there is also a global context.


We can also add actions and guards to the model, which can affect how the model is walked and how it behaves:

...

In sum, we model (i.e. build a model) a certain aspect related to our system using directed graphs; the model represents a test idea that describes expected behaviors. Checks are implemented in the vertices (i.e. states) and actions are performed in the edges. GraphWalker AltWalker will then "walk" the model (i.e. perform a set of "steps"/edges) using a generated path from GraphWalker. While doing so, it looks at JavaScript guards to check is edges can be "walked" and performs JavaScript based actions to set internal context variables . It stops "walking" if stop condition(s) are met.

To build the model, we can either use a visual tool (AltomAltWalker's Model-Editor, or GraphWalker Studio) and export it to a JSON file, or an IDE instead (e.g. VSCode with a specific extension).

...

When we "execute the model", it will walk the path (i.e. go over from vertex to vertex through a given edge) and performing checks in the vertices. If those checks are successful until the stop condition(s) is achivied, we can say that it was successful; otherwise, the model is not a good representation of the system as it is and we can say that it "failed".

Example

In this tutorial, we'll use an is based on example provided by the GraphWalker community (please check GraphWalker wiki page describing it) which targets the well-known PetClinic sample site.

This example has been ported from GraphWalker+Java to AltWalker+Python and the full source-code is available here.

Image RemovedImage Added


Requirements

  • Target SUT (PetClininc sample application):
    • Java 8 
    PetClinic sample application (requires Java 8 as it is)
    • source-code
      • git clone https://github.com/SpringSource/spring-petclinic.git
        cd spring-petclinic
        git reset --hard 482eeb1c217789b5d772f5c15c3ab7aa89caf279
        mvn tomcat7:run
  • Test code (source-code and additional details here)
    • GraphWalker
    GraphWalker
    • 4.2.0
    • AltWalker 0.2.7
    • Altom's Model-Editor or GraphWalker Studio


How can we test the PetClinic using MBT technique?

...

Info
titlePlease note
Remember that you could model it completely differently; modeling represents a perspective.


Models As mentioned earlier, models can be built using GraphWalker Studio. We using AltWalker's Model-Editor (or GraphWalker Studio) or directly in the IDE (for VSCode there's a useful extension to preview it). In the visual editors, namelly in AltWalker's Model-Editor, we can use it to load previously saved model(s) like the ones in PetClinicpetclinic_full.json. In this case, the JSON file contains several models; we could also have one JSON file per model.

The following picture shows the overall PetClinic model, that interacts with other models, and also the NewOwner model.

Image Added  Image Added


If we use the visual editors to build the model, then we need to export it to one (or more) JSON file(s). 

Image Removed

Image Added   Image Added


Note: if you use GraphWalker Studio instead, it allows you GraphWalker Studio allow us to run the model in offline, i.e. without executing the underlying test automation code, so we can validate it.

...

Otherwise, if we fill incorrect data (i.e. using the edge "e_IncorrectData") an error will be shown and the user keeps on the "New Owner" page.


Image RemovedImage Added



Usually, to implement the automation code we would create a Maven project from scratch, copy the model file(s), and generate a skeleton of the sources for our model.

To do so, we would perform something such as:

# generate a Maven project prepared for GraphWalker
mvn archetype:generate -B -DarchetypeGroupId=org.graphwalker -DarchetypeArtifactId=graphwalker-maven-archetype -DgroupId=com.company -DartifactId=myProject
# store the JSON of the model(s) in src/main/resources/
...# generate a skeleton of an implementable interface
mvn graphwalker:generate-sources
Info
titlePlease note

As detailed in AltWalker's documentation, if we start from scratch (i.e. without a model), we can initialize a project for our automation code using something like:


  $ altwalker init -l python test-project


When we have the model, we can generate the test package containing a skeleton for the underlying test code.


  $ altwalker generate -l python path/for/test-project/ -m path/to/models.json


If we do have a model, then we can pass it to the initialization command:


  $ altwalker init -l python test-project -m path/to/model-name.json



During implementation, we can check our model for issues/inconsistencies, just from a modeling perspective:


  $ altwalker check -m path/to/model-name.json "random(vertex_coverage(100))"


We can also check verify if the test package contains the implementation of the code related to the vertices and edges.


  $ altwalker verify -m path/to/model-name.json tests


Check the full syntax of AltWalker's CLI (i.e. "altwalker") for additional details.



The  Java class that implements the edges and vertices of this model is defined in the class NewOwnerTest. Actions performed in the edges The Java class that implements the edges and vertices of this model is defined in the class NewOwnerTest. Actions performed in the edges are quite simple. Assertions are also simple as they're only focused on the state/vertex they are at.

Code Block
languagejavapy
titleclass implementing the model "NewOwner"
collapsetrue
tests/test.py (main code with the tests)
collapsetrue
import unittest

from selenium import webdriver
from selenium.webdriver.firefox.options import Options

from tests.pages.base import BasePage
from tests.pages.home import HomePage
from tests.pages.find_owners import FindOwnersPage
from tests.pages.owners import OwnersPage
from tests.pages.new_owner import NewOwnerPage
from tests.pages.veterinarians import VeterinariansPage
from tests.pages.owner_information import OwnerInformationPage

import sys
import pdb
from faker import Faker

debugger = pdb.Pdb(skip=['altwalker.*'], stdout=sys.stdout)
fake = Faker()

HEADLESS = False
BASE_URL = "http://localhost:9966/petclinic"

driver = None


def setUpRun():
    """Setup the webdriver."""

    global driver

    options = Options()
    if HEADLESS:
        options.add_argument('-headless')

    print("Create a new Firefox session")
    driver = webdriver.Firefox(options=options)

    print("Set implicitly wait")
    driver.implicitly_wait(15)
    print("Window size: {width}x{height}".format(**driver.get_window_size()))

def tearDownRun():
    """Close the webdriver."""

    global driver

    print("Close the Firefox session")
    driver.quit()

class BaseModel(unittest.TestCase):
	"""Contains common methods for all models."""

	def setUpModel(self):
		global driver
		print("Set up for: {}".format(type(self).__name__))
		self.driver = driver

	def v_HomePage(self):
		page = HomePage(self.driver)
		self.assertEqual(page.heading_text, "Welcome", "Welcome heading should be present")
		self.assertTrue(page.is_footer_present, "footer should be present")
	
	def v_FindOwners(self):
		page = FindOwnersPage(self.driver)
		self.assertEqual("Find Owners",page.heading_text, "Find Owners heading should be present")
		self.assertTrue(page.is_footer_present, "footer should be present")

	def v_NewOwner(self):
		page = NewOwnerPage(self.driver)
		self.assertEqual( "New Owner",page.heading_text, "New Owner heading should be present")
		#$x("/html/body/table/tbody/tr/td[2]/img").shouldBe(visible);
		self.assertTrue(page.is_footer_present, "footer should be present")

	def v_Owners(self):
		page = OwnersPage(self.driver)
		self.assertEqual("Owners",page.heading_text, "Owners heading should be present")
		self.assertGreater(page.total_owners_in_list, 9, "Owners in listing >= 10")

	def v_Veterinarians(self):
		page = VeterinariansPage(self.driver)
		self.assertEqual(page.heading_text,"Veterinarians", "Veterinarians heading should be present")
		self.assertTrue(page.is_footer_present, "footer should be present")

	def v_OwnerInformation(self, data):
		page = OwnerInformationPage(self.driver)
		self.assertEqual(page.heading_text, "Owner Information", "Owner Information heading should be present")
		data["numOfPets"] = page.number_of_pets
		print(f"numOfPets: {page.number_of_pets}")
		self.assertTrue(page.is_footer_present, "footer should be present")

	def e_DoNothing(self, data):
		#debugger.set_trace()
		pass

	def e_FindOwners(self):
		page = BasePage(self.driver)
		page.click_find_owners()


class PetClinic(BaseModel):
	def e_StartBrowser(self):
		page = HomePage(self.driver, BASE_URL)
		page.open()

	def e_HomePage(self):
		page = HomePage(self.driver)
		page.click_home()

	def e_Veterinarians(self):
		page = HomePage(self.driver)
		page.click_veterinarians()

	def e_FindOwners(self):
		page = HomePage(self.driver)
		page.click_find_owners()

class FindOwners(BaseModel):

	def e_AddOwner(self):
		page = FindOwnersPage(self.driver)
		page.click_add_owner()

	def e_Search(self):
		page = FindOwnersPage(self.driver)
		page.click_submit()


class OwnerInformation(BaseModel):
	def e_UpdatePet(self):
		page = OwnerInformationPage(self.driver)
		page.click_submit()

	def e_AddPetSuccessfully(self):
		page = OwnerInformationPage(self.driver)
		page.fillout_pet(fake.name(),fake.past_date().strftime("%Y/%m/%d"), "dog")
		page.click_submit()

	def e_AddPetFailed(self):
		page = OwnerInformationPage(self.driver)
		page.fillout_pet("",fake.past_date().strftime("%Y/%m/%d"), "dog")
		page.click_submit()

	def e_AddNewPet(self):
		page = OwnerInformationPage(self.driver)
		page.click_add_new_pet()

	def e_EditPet(self):
		page = OwnerInformationPage(self.driver)
		page.click_edit_pet()

	def e_AddVisit(self):
		page = OwnerInformationPage(self.driver)
		page.click_add_visit()

	def v_NewPet(self):
		page = OwnerInformationPage(self.driver)
		self.assertEqual(page.heading_text, "New Pet", "New Pet heading should be present")
		self.assertTrue(page.is_footer_present, "footer should be present")

	def v_NewVisit(self):
		page = OwnerInformationPage(self.driver)
		self.assertEqual(page.heading_text, "New Visit", "New Visit heading should be present")
		self.assertTrue(page.is_visit_visible, "visit should be present")
	
	def e_VisitAddedSuccessfully(self):
		page = OwnerInformationPage(self.driver)
		page.clear_description()
		page.set_description(fake.name())
		page.click_submit()

	def e_VisitAddedFailed(self):
		page = OwnerInformationPage(self.driver)
		page.clear_description()
		page.click_submit()

	def v_Pet(self):
		page = OwnerInformationPage(self.driver)
		self.assertEqual(page.heading_text, "Pet", "Pet heading should be present")

class Veterinarians(BaseModel):
	def e_Search(self):
		page = VeterinariansPage(self.driver)
		page.search_for("helen")

	def v_SearchResult(self):
		page = VeterinariansPage(self.driver)
		self.assertTrue(page.is_text_present_in_vets_table, "Helen Leary")
		self.assertTrue(page.is_footer_present, "footer should be present")

	def v_Veterinarians(self):
		page = VeterinariansPage(self.driver)
		self.assertEqual(page.heading_text,"Veterinarians", "Veterinarians heading should be present")
		self.assertGreater(page.number_of_vets_in_table, 0, "At least one Veterinarian should be listed in table")

class NewOwner(BaseModel):

	def e_CorrectData(self):
		page = NewOwnerPage(self.driver)
		page.fill_owner_data(first_name=fake.first_name(), last_name=fake.last_name(), address=fake.address(), city=fake.city(), telephone=fake.pystr_format('##########'))
		#page.fill_telephone(fake.pystr_format('##########'))
		page.click_submit()

	def e_IncorrectData(self):
		page = NewOwnerPage(self.driver)
		page.fill_owner_data()
		#page.fill_telephone("12345678901234567890")
		page.fill_telephone(fake.pystr_format('####################'))
		page.click_submit()

	def v_IncorrectData(self):
		page = NewOwnerPage(self.driver)
		self.assertTrue(page.error_message, "numeric value out of bounds (<10 digits>.<0 digits> expected")package com.company.modelimplementations;

import com.company.NewOwner;
import com.github.javafaker.Faker;
import org.graphwalker.core.machine.ExecutionContext;
import org.graphwalker.java.annotation.GraphWalker;
import org.openqa.selenium.By;

import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$x;

/**
 * Implements the model (and interface) NewOwnerSharedState
 * The default path generator is Random Path.
 * Stop condition is 100% coverage of all edges.
 */
@GraphWalker(value = "random(edge_coverage(100))")
public class NewOwnerTest extends ExecutionContext implements NewOwner {

    @Override
    public void v_OwnerInformation() {
        $(By.tagName("h2")).shouldHave(text("Owner Information"));
        $x("/html/body/div/table[last()]/tbody/tr/td[2]/img").shouldBe(visible);
    }

    @Override
    public void e_CorrectData() {
        fillOwnerData();
        $(By.id("telephone")).sendKeys(String.valueOf(new Faker().number().digits(10)));
        $("button[type=\"submit\"]").click();
    }

    @Override
    public void e_IncorrectData() {
        fillOwnerData();
        $(By.id("telephone")).sendKeys(String.valueOf(new Faker().number().digits(20)));
        $("button[type=\"submit\"]").click();
    }

    @Override
    public void v_IncorrectData() {
        $(By.cssSelector("div.control-group.error > div.controls > span.help-inline"))
                .shouldHave(text("numeric value out of bounds (<10 digits>.<0 digits> expected)"));
    }

    @Override
    public void v_NewOwner() {
        $(By.tagName("h2")).shouldHave(text("New Owner"));
        $x("/html/body/table/tbody/tr/td[2]/img").shouldBe(visible);
    }

    private void fillOwnerData() {
        $(By.id("firstName")).clear();
        $(By.id("firstName")).sendKeys(new Faker().name().firstName());

        $(By.id("lastName")).clear();
        $(By.id("lastName")).sendKeys(new Faker().name().lastName());

        $(By.id("address")).clear();
        $(By.id("address")).sendKeys(new Faker().address().fullAddress());

        $(By.id("city")).clear();
        $(By.id("city")).sendKeys(new Faker().address().city());

        $(By.id("telephone")).clear();
    }
}



In the previous example, we can see that the class NewOwnerTest extends ExecutionContext; this ties the model with the path generator and provides a context for tracking the internal state and history of the model.

...

...