16 грудня 2021 року відбувся GlobalLogic Test Automation Online TechTalk “Test Driven Development as a Personal Skill”! Анатолій Сахно (Software Testing Consultant, GlobalLogic) розібрав принципи TDD (розробки, керованої тестами) та приклади їх застосування. Крім того, поговорили про:
- Ефективне використання модульних тестів у повсякденних задачах;
- Використання TDD при розробці тестових фреймворків;
- Застосування принципів TDD при написанні функціональних автотестів.
Більше про захід: https://www.globallogic.com/ua/about/events/globallogic-test-automation-online-techtalk-test-driven-development-as-a-personal-skill/
Приємного перегляду і не забудьте залишити коментар про враження від TechTalk!
Ця активність — частина заходів в рамках GlobalLogic Test Automation Advent Calendar, ще більше заходів та цікавинок за посиланням: https://bit.ly/AdventCalendar_fb
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
GlobalLogic Test Automation Online TechTalk “Test Driven Development as a Personal Skill”
1. 1
Confidential
TDD as a Personal Skill
Anatoliy Sakhno is a Test Architect with a solid background in
Healthcare Quality Assurance and experience in software
development and test automation.
Write a
failing
test
Make it
pass
Refactor
3. 3
Confidential
Integration Test
Fluent and consistent
Test Pyramid
End-to-end Test
Complex, beautiful, and fragile
Unit Test
Solid and valuable
Does the whole system work?
Does our code works against the
code we cannot change?
Do our objects do the right thing,
are they convenient to work with?
*Growing Object-Oriented Software, Guided by Tests 1st Edition by Steve Freeman and Nat
Pryce
9. 9
Confidential
Sluggish debug practices:
Breakpoints
Step-by-step execution
Reuse of e2e tests (as is, or by commenting out the
parts that are out of focus)
Use of temporary tests or scripts
(TestRegistrationDebug.java, temp11.py, etc.)
First Rule of Debugging: Don’t Use the Debugger
10. 10
Confidential
Effective Debug: Make feedback loop even shorter
Unit Test
Meaningful Custom Exception
[Debug] and [Verbose] logging
Clean Code & Patterns-Driven Objects
11. 11
Confidential
Refactoring
• Rename or move code elements
• Extract method (AQA: Extracting methods to BaseTest class is a bad pattern*)
• Extract class (AQA: Extracting classes as Helpers is a bad pattern**)
• Apply design pattern (AQA: DTO, Builders, Factories is your bare minimum)
* Bloated BaseTest is a Large Class antipattern
** Helper name hides the real purpose of a class. Example: DeviceHelper could implement DeviceBuilder,
DeviceFactory, DeviceEmulator, DeviceConfiguration, DeviceDataGenerator or mix of above
Code refactoring is the process of restructuring existing code without changing its external behavior.
12. 12
Confidential
Benefits of having unit tests while coding
• Code execution runs take less time
• Coding focus is not interrupted (flow state makes you more
effective)
• Less metathesiophobia (fear of changes)
• Better code (due to refactoring-friendly environment)
With unit tests the refactoring usually occurs before the code becomes unsupportable
13. 13
Confidential
Quality Gate
TAF quality gate usually includes:
- Some smoke tests;
- A subset (or full set) of integration tests;
- Validation unit tests;
Note: Usually you do have functional test suite which can be
reused as a quality gate for the TAF codebase. But such suites
takes more time to execute and might include unrelated failures.
A quality gate is a set of conditions that needs to be met before you can merge your commit into a
protected branch (for example, the master)
15. 15
Confidential
TDD: create a dedicated test for any task (class) I am going to
spend more than 30 minutes. After debug is done, some delete the
test if you don’t see it’s value for refactoring/quality gate/validation.
Refactor as you go: write a validation/unit test for the objects (API
Wrapper, Business Layer Objects, Configuration Objects (Helpers),
Data Access & Data Generation objects, etc.) which are used by
your tests more than 5 times.
I don’t create tests for simple functions and objects. Also there is no
need to create tests for page objects unless you need to handle untrivial
UI behavior.
My TDD Rules
16. 16
Confidential
TDD Test Automation System Buildout
End-to-end Test
Integration Test (Isolated
API tests)
Framework Validation Unit Test
Does the whole system work?
Does our code works against the
code we cannot change?
Do framework objects do the right
thing, are they convenient to work
with?
22. 22
Confidential
Example: Unit Test as HealthCheck
public class RequestGeneratorValidation {
private User doctor = null;
@BeforeClass(alwaysRun = true)
@Parameters({ "Username", "Password" })
public void BeforeClass(String Username, String Password, ITestContext context) {
doctor = new User(Username, Password);
AppConfig.loadConfig();
}
@Test(groups = { "smoke", "validation", "aws" })
public void validateRequestsGeneratorHealth_Aws() {
AppConfig.setIsAWSMode(true);
RequestGenerator generator = new RequestGenerator(AppConfig.getWebDriver(),
AppConfig.getHospitalUrl());// AWS Device Farm Desktop
assert generator.healthCheck(doctor);
}
@Test(groups = { "smoke", "validation" })
public void validateRequestsGeneratorHealth_Local() {
AppConfig.setIsAWSMode(false);
RequestGenerator generator = new RequestGenerator(AppConfig.getWebDriver(),
AppConfig.getHospitalUrl()); // local selenium driver
assert generator.healthCheck(doctor);
}
}
23. 23
Confidential
Example: Make it work from test (1)
@pytest.mark.validation
def test_advertising_manufacture_data(
device_type=1002,
serial_number=203030142,
expected_manufacture_data="41:45:02:0c:19:f1:7e:3b:ba:03:01:0b",
):
config = EmulatorConfig("config.ini")
device_info = DeviceInfoFactory().create_device_info(
device_type=device_type, firmware_revision=config.firmware_revision
)
manufacture_data = (
struct.pack("HB", config.company_id, device_info.product_id)
+ struct.pack(">L", serial_number)
+ struct.pack(">H", device_type)
+ struct.pack(">L", device_info.firmware_revision)[1:]
)
logging.debug(
f"manufacture_data: {','.join( '0x%02x' %i for i in manufacture_data) }"
)
assert ":".join("%02x" % i for i in manufacture_data) == expected_manufacture_data
24. 24
Confidential
Example: Make it work from test (2)
@pytest.mark.validation
def test_advertising_manufacture_data(
device_type="1002",
serial_number=203030142,
expected_manufacture_data="41:45:02:0c:19:f1:7e:3b:ba:03:01:0b",
):
config = EmulatorConfig("config.ini")
device_info = DeviceInfoFactory().create_device_info(
device_type=device_type, firmware_revision=config.firmware_revision
)
device_info.serial_number = serial_number
emulator = deviceEmulator(
config=config,
device_info=device_info,
)
manufacture_data = emulator.get_advertising_string()
assert ":".join("%02x" % i for i in manufacture_data) == expected_manufacture_data
25. 25
Confidential
Example: Walking Skeleton
A Walking Skeleton is a tiny implementation of the system that performs a small end-to-end function.
interface User {
public User register(Application app);
public void unregister();
public User login();
public void logout();
public Something createSomething(Something something);
public Something modifySomething(Something something);
public void deleteSomething(UUID somethingUuid);
}
public class SomethingTest {
public SomethingTest() {
app = GlobalConfig.getApp();
}
@Test(groups = { "validation", "fake" })
public void mockCreateSomething() {
// arrange
User user = new MockUser("Odike, Prince of Nigeria",
"Odike.II@nigeria.gov.ng");
user.register(app)
.login();
// act
Something thing = user.createSomething(new
Something("Special"));
// assert
Assert.assertEquals(thing.getName(), "Special");
}
}
public class UiUser implements User {}
public class ApiUser implements User {}
public class MockUser implements User {}
28. 28
Confidential
Test Driven Test Automation
Write a
failing test
Make it work Refactor
Apply Test
Design
Technique
Make it
work, not
pass :)
29. 29
Confidential
[Story-007] As a Customer I want to add items to the cart So that I prefill my order
public class ShoppingCartTest {
private WebStoreMarket market = GlobalConfig.getMarket();
@Test(groups = { "presentation",
"story-007" }, description = "verify that two items can be added to cart")
public void add2ItemsToCart() {
// ARRANGE
WebStore store = new WebStore("Юшка & Петрушка").register(market);
Product soup = new Product("Юшка", 5.99, ProductType.Grocery);
store.publishProduct(soup, 500);
User customer = new User("Финтик Каленик Кононович",
UserType.RegularCustomer).register(market);
customer.login();
// ACT
customer.addToCart(customer.findProduct(soup), 2);
// ASSERT
List<CartItem> expectedItems = new List<CartItem>() {
{add(new CartItem(soup), 2);}
};
Assert.assertEquals(customer.getCart().getItems(), expectedItems);
}
}
1. SRP is violated
2. Data is hardcoded
3. Test design
techniques cannot be
applied
Typical Approach:
30. 30
Confidential
[Story-007] As a Customer I want to add items to the cart So that I prefill my order
// Step 1: Write a failing test
public class ShoppingCartTest {
private WebStoreMarket market;
public ShoppingCartTest() {
market = GlobalConfig.getMarket();
}
@Test(groups = { "presentation", "story-007" }, description = "verify that items can be added to cart")
public void addItemsToCart(User customer, List<Product> items) {
// ARRANGE
customer.login();
// ACT
for (Product item : items) {
customer.addToCart(customer.findProduct(item), 1);
}
// ASSERT
Cart expectedCart = new Cart(products);
Assert.assertEquals(customer.getCart(), expectedCart);
}
}
31. 31
Confidential
[Story-007] As a Customer I want to add items to the cart So that I prefill my order
// Step 2: Make it Work
@Test(groups = { "presentation",
"story-007" }, dataProvider = "
shopping-cart-cases", description = "verify that
items can be added to cart")
public void addItemsToCart(User customer,
List<Product> items) {
// ARRANGE
customer.login();
// ACT
for (Product item : items) {
customer.addToCart(customer.findProduc
t(item), 1);
}
// ASSERT
Cart expectedCart = new Cart(items);
Assert.assertEquals(customer.getCart(),
expectedCart);
}
@DataProvider(name = "shopping-cart-cases")
public Object[][] shoppingCartCases() {
WebStore store = new WebStore("Юшка & Петрушка")
.register(market);
Product soup = new Product("Юшка", 5.99,
ProductType.Grocery);
store.publishProduct(soup, 500);
User customer = new User("Финтик Каленик Кононович",
UserType.RegularCustomer).register(market);
ArrayList<Product> soup = new ArrayList<Product>() {
{
add(soup);
}
};
return new Object[][] { { customer, soup }};
}
It works, but SRP is still violated
32. 32
Confidential
[Story-007] As a Customer I want to add items to the cart So that I prefill my order
// Step 3: Refactor
@Test(groups = { "presentation",
"story-007" }, dataProvider = " shopping-cart-cases", description = "verify that items can be added to cart")
public void addItemsToCart(User customer, List<Product> items) {
// ARRANGE
customer.login();
// ACT
for (Product item : items) {
customer.addToCart(customer.findProduct(item), 1);
}
// ASSERT
Cart expectedCart = new Cart(items);
Assert.assertEquals(customer.getCart(), expectedCart);
}
@DataProvider(name = "shopping-cart-cases")
public Object[][] shoppingCartCases() {
ProductFactory productFactory = new ProductFactory(new WebStore(“[Story-007] Юшка & Петрушка"), market);
UserFactory userFactory = new UserFactory(market);
return new Object[][] {{ userFactory.getUser(UserType.RegularCustomer), productFactory.getGroceryItem("Юшка") }};
}
33. 33
Confidential
[Story-007] As a Customer I want to add items to the cart So that I prefill my order
// Step 4: Apply Test Design Technique
@Test(groups = { "presentation",
"story-007" }, dataProvider = " shopping-cart-cases", description = "verify that items can be added to cart")
public void addItemsToCart(User customer, List<Product> items) {
// ARRANGE
customer.login();
// ACT
for (Product item : items) {
customer.addToCart(customer.findProduct(item), 1);
}
// ASSERT
Cart expectedCart = new Cart(items);
Assert.assertEquals(customer.getCart(), expectedCart);
}
@DataProvider(name = "shopping-cart-cases")
public Object[][] shoppingCartCases() {
// loop through user types and basic product selection options
ProductFactory productFactory = new ProductFactory(new WebStore(“[Story-007] Юшка & Петрушка"), market);
UserFactory userFactory = new UserFactory(market);
return new Object[][] {
{ userFactory.getUser(UserType.RegularCustomer), productFactory.getStandardMenu(1) },
{ userFactory.getUser(UserType.PremiumCustomer), productFactory.getAdultMenu(1) },
{ userFactory.getUser(UserType.MinorCustomer), productFactory.getProduct("Юшка", 1) },
{ userFactory.getUser(UserType.SystemAdministrator), productFactory. getStandardMenu(2) },
};
}