Page History
...
Generators and stop conditions are essential in AltWalker & GraphWalker (more info here, here, 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 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.
Requirements
- Target SUT (PetClininc sample application):
- Java 8
- 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
- 4.2.0
- AltWalker 0.2.7
- Altom's Model-Editor or GraphWalker Studio
How can we test the PetClinic using MBT technique?
...
Info | ||
---|---|---|
| ||
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.
If we use the visual editors to build the model, then we need to export it to one (or more) JSON file(s).
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.
Info | ||
---|---|---|
| ||
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 | |||||||||
---|---|---|---|---|---|---|---|---|---|
| |||||||||
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.
...
- AltWalker
- Visual model editor for AltWalker and GraphWalker
- AltWalker Model Visualizer for VSCode
- Actions and Guards (from AltWalker's documentation)
- AltWalker examples (Python and C#/.NET)
- AltWalker CLI
- Port of PetClinic MBT example to AltWalker and Python
- GraphWalker models for testing the PetClinic site (source-code)
...