Upload
letagilefly
View
1.474
Download
1
Embed Size (px)
DESCRIPTION
讲师:姚若舟 拥有十多年的软件开发经验和多年的项目管理经验,目前任职于欧特克(Autodesk)中国研发中心担任项目经理,提供内部团队敏捷实践的指导和培训。 作为组织者参与了2011年敏捷之旅上海的组织工作。同时在敏捷社区的活动中,做过多次有关测试驱动开发的分享,并通过组织Coding Dojo来推广测试驱动开发和结对编程。 个人喜爱Coding Kata,关注如何将测试驱动开发和相关工程实践更好的结合到公司的实际项目开发中,以及如何成为一个杰出的Scrum Master和敏捷教练来帮助团队和组织成长 话题介绍: 希望通过这个演讲,可以让听众明白如何在遗留代码基础上通过简单有效的设计和隔离来给新代码添加单元测试,从而让团队停止产生相应的技术负债。而且,如何拥有具备可测性的设计和做到有效的隔离,并不像有些人想的那么复杂和困难。
Citation preview
How to Write Unit Test for
New Code on top of Legacy Code
Joseph Yao
Please Read This Great Book!
Unit Test 101
What’s Unit Test?
Unit tests is the idea that they are tests in
isolation of individual components of software
- Michael C. Feathers
What’s Legacy Code?
Legacy code is simply code without tests.
Without tests is bad code. It doesn't matter how well written
it is; it doesn't matter how pretty or object-oriented or well-
encapsulated it is. With tests, we can change the behavior
of our code quickly and verifiably. Without them, we really
don't know if our code is getting better or worse.
- Michael C. Feathers
Make New Code Testable
How to Initialize Dependency
Need
Legacy Code
New Code
Your Code Looks Like This? public class Car {
private Engine engine;
public Car() {
engine = new Engine();
}
public void run() {
engine.start();
}
public String status() {
return engine.speed() > 0 ? “Move”: “Stop”;
}
}
Your Test Looks Like This?
Are you really do the unit test for Car?
public class TestCar {
@Test public void move() {
Car car = new Car();
car.run();
assertEquals(“Move”, car.status());
}
}
Code for Car with Isolation public class Car {
private IEngine engine;
public Car(IEngine theEngine) {
engine = theEngine;
}
public void run() {
engine.start();
}
public String status() {
return engine.speed() > 0 ? “Move”: “Stop”;
}
}
Test for Car with Isolation public class TestCar {
@Test public void move() {
IEngine engineMock =
new EngineMock(10);
Car car = new Car(engineMock);
car.run();
assertEquals(“Move”, car.status());
}
}
In fact, you don’t care how engine speed is calculated
I Need to Test a Private Method
Your Code Looks Like This?
How can I test the private method?
public class Order {
public void addItem(Item item) {
if (isValidItem(item)) {
items.add(item);
}
}
private boolean isValidItem(Item item) {
more than 500 lines code…
}
}
Does Your Class Have too many Responsibilities?
Maybe This Code is Better? public class Order {
private ItemValidator validator;
public Order (ItemValidator
theValidator) {
validator = theValidator;
}
public void addItem (Item item) {
if (validator.
isValidItem(item)) {
…
}
}
}
Single Responsibility Principle
“Don’t Do it” unless you have a Very Strong Reason
+
Good design is testable, and
design that isn't testable is bad
- Michael C. Feathers
Isolate Legacy Code
Too hard to get Legacy Code under Test
Some New Code needed to be added
public class Customer {
…
public void purchase() {
more than 500 lines of legacy code…
}
…
}
We need to log this customer purchase action after it done.
Can you add unit test for new code with no impact to legacy code?
Add new code by Sprout Method
public class Customer {
public void purchase() {
purchaseWithoutLog();
logPurchaseAction();
}
protected void purchaseWithoutLog() {
more than 500 lines of legacy code…
}
private void logPurchaseAction() {
Your new code here…
}
}
Your Test May Look Like This public class TestCustomerPurchaseLog extends
Customer {
@Test public void log() {
Customer customer =
new TestCustomerPurchaseLog();
customer.purchase();
… code to verify the log action …
}
protected void purchaseWithoutLog() {}
}
Some other Alternative Ways
• Sprout Method – the Sample Code
• Wrap Method
• If the legacy class is hard to be put into test
harness
o Sprout Class
o Wrap Class
o This is quite useful when you can’t easily isolate the
dependency for legacy class
I have a Monster Dependency to Isolate
How to Isolate “HttpServletRequest”?
public class ARMDispatcher {
public void populate (HttpServletRequest request) {
String [] values
= request.getParameterValues(pageStateName);
if (values != null && values.length > 0) {
marketBindings.put(
pageStateName + getDateStamp(), values[0]);
}
}
}
You only Need to get the Parameter Value
public class ARMDispatcher {
public void populate (ParameterSource source) {
String value =
source.getParameterForName(pageStateName);
if (value != null) {
marketBindings.put(
ageStateName + getDateStamp(), value);
}
}
}
I need D, but it’s from A.getB().getC().getD()
Your Code may Look like this public class Customer {
private Orders orders;
public List<String> getAllItemNamesOfLatestOrder(){
List<String> allNames =
new ArrayList<String>();
Item[] items =
orders.getLatestOrder().getAllItems();
for (Item item : items) {
allNames.add(item.getName());
}
return allNames;
}
}
You need to Isolate the way to Get Items
public class Customer {
private Orders orders;
public List<String> getAllItemNamesOfLatestOrder(){
List<String> allNames =
new ArrayList<String>();
Item[] items = getAllItemsOfLatestOrder();
for (Item item : items) {
allNames.add(item.getName());
}
return allNames;
}
protected Item[] getAllItemsOfLatestOrder() {
return orders.getLatestOrder().getAllItems();
}
}
Suggestions
Test Behavior
instead of
Test Implementation
Dependency Isolation Tool is
Evil
Q & A
新浪微博 @姚若舟
TDD Code Kata @tudou.com/home/yaoruozhou