测试遗留代码:有线依赖

与一些开发人员结对时,我注意到他们不对现有代码进行单元测试的原因之一是,因为他们经常不知道如何克服某些问题。 最常见的问题与硬性依赖关系有关– Singleton静态调用

让我们看一下这段代码:

public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
    List<Trip> tripList = new ArrayList<Trip>();
    User loggedUser = UserSession.getInstance().getLoggedUser();
    boolean isFriend = false;
    if (loggedUser != null) {
        for (User friend : user.getFriends()) {
            if (friend.equals(loggedUser)) {
                isFriend = true;
                break;
            }
        }
        if (isFriend) {
            tripList = TripDAO.findTripsByUser(user);
        }
        return tripList;
    } else {
        throw new UserNotLoggedInException();
    }
}

太可怕了,不是吗? 上面的代码有很多问题,但是在进行更改之前,我们需要将其包含在测试中。

在对上述方法进行单元测试时,存在两个挑战。 他们是:

User loggedUser = UserSession.getInstance().getLoggedUser(); // Line 3  
   
tripList = TripDAO.findTripsByUser(user);                    // Line 13

众所周知,单元测试应该只测试一个类而不是其依赖项。 这意味着我们需要找到一种模拟Singleton和静态调用的方法。 通常,我们这样做是注入依赖项,但是我们有一条规则 ,还记得吗?

如果测试未涵盖,我们将无法更改任何现有代码。 唯一的例外是,如果我们需要更改代码以添加单元测试,但是在这种情况下,仅允许自动重构(通过IDE)。

除此之外,许多模拟框架无论如何都无法模拟静态方法,因此注入TripDAO不能解决问题。

克服硬依赖性问题

注意:在现实生活中,我将首先编写测试并在需要时进行更改,但是 为了使发布简短而专心, 我在这里不会逐步进行。

首先,让我们隔离Singleton依赖于它自己的方法。 让我们也对其进行保护。 但是,等等,这需要通过自动的“提取方法”重构来完成。 在TripService.java上仅选择以下代码:

UserSession.getInstance().getLoggedUser()

转到IDE的重构菜单,选择解压缩方法并为其命名。 完成此步骤后,代码将如下所示:

public class TripService {

    public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
        ...
        User loggedUser = loggedUser();
        ...
    }

    protected User loggedUser() {
        return UserSession.getInstance().getLoggedUser();
    }
}

对TripDAO.findTripsByUser(user)做同样的事情,我们将有:

public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
    ...
    User loggedUser = loggedUser();
    ...
        if (isFriend) {
            tripList = findTripsByUser(user);
        }
    ...
}  
 
protected List<Trip> findTripsByUser(User user) {
    return TripDAO.findTripsByUser(user);
} 
 
protected User loggedUser() {
    return UserSession.getInstance().getLoggedUser();
}

在测试类中,我们现在可以扩展TripService类,并覆盖我们创建的受保护方法,使它们返回我们进行单元测试所需的内容:

private TripService createTripService() {
    return new TripService() {
        @Override protected User loggedUser() {
            return loggedUser;
        }
        @Override protected List<Trip> findTripsByUser(User user) {
            return user.trips();
        }
    };
}

就是这样。 我们的TripService现在可以测试了。

首先,我们编写所有需要的测试,以确保对类/方法进行了全面测试,并执行了所有代码分支。 我为此使用Eclipse的eclEmma插件 ,强烈建议使用。 如果您未使用Java和/或Eclipse,请在编写现有代码测试时尝试使用特定于您的语言/ IDE的代码覆盖工具。 这很有帮助。

所以这是我的最终测试课:

public class TripServiceTest {
        
    private static final User UNUSED_USER = null;
    private static final User NON_LOGGED_USER = null;
    private User loggedUser = new User();
    private User targetUser = new User();
    private TripService tripService;

    @Before
    public void initialise() {
        tripService  = createTripService();
    } 
        
    @Test(expected=UserNotLoggedInException.class) public void 
    shouldThrowExceptionWhenUserIsNotLoggedIn() throws Exception {
        loggedUser = NON_LOGGED_USER;
                 
        tripService.getTripsByUser(UNUSED_USER);
    }
        
    @Test public void 
    shouldNotReturnTripsWhenLoggedUserIsNotAFriend() throws Exception {             
        List<Trip> trips = tripService.getTripsByUser(targetUser);
                 
        assertThat(trips.size(), is(equalTo(0)));
    }
        
    @Test public void 
    shouldReturnTripsWhenLoggedUserIsAFriend() throws Exception {
        User john = anUser().friendsWith(loggedUser)
                            .withTrips(new Trip(), new Trip())
                            .build();
                 
        List<Trip> trips = tripService.getTripsByUser(john);
                 
        assertThat(trips, is(equalTo(john.trips())));
    }

    private TripService createTripService() {
        return new TripService() {
            @Override protected User loggedUser() {
                return loggedUser;
            }
            @Override protected List<Trip> findTripsByUser(User user) {
                return user.trips();
            }
        };
    }        
}

我们完了吗?

当然不是。 我们仍然需要重构TripService类。

public class TripService {

        public List<Trip> getTripsByUser(User user) throws   
                                       UserNotLoggedInException {
               List<Trip> tripList = new ArrayList<Trip>();
               User loggedUser = loggedUser();
               boolean isFriend = false;
               if (loggedUser != null) {
                       for (User friend : user.getFriends()) {
                             if (friend.equals(loggedUser)) {
                                     isFriend = true;
                                     break;
                             }
                       }
                       if (isFriend) { 
                             tripList = findTripsByUser(user);
                       }
                       return tripList;
               } else {
                      throw new UserNotLoggedInException();
               }
        }

        protected List<Trip> findTripsByUser(User user) {
            return TripDAO.findTripsByUser(user);
        }

        protected User loggedUser() {
            return UserSession.getInstance().getLoggedUser();
        }

}

您可以看到多少个问题? 请花一些时间阅读我发现的内容。:-)

重构
 
注意完成此操作后,我会在每个步骤之后逐步运行测试,以完成此操作。 在这里,我将总结一下我的决定

我注意到的第一件事是,当登录的用户为null时,不需要创建tripList变量,因为会引发异常并且什么也没有发生。 我决定反转外部if并提取guard子句

public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
        User loggedUser = loggedUser();
        validate(loggedUser);
        List<Trip> tripList = new ArrayList<Trip>();
        boolean isFriend = false;
        for (User friend : user.getFriends()) {
               if (friend.equals(loggedUser)) {
                      isFriend = true;
                      break;
               }
        }
        if (isFriend) {
                tripList = findTripsByUser(user);
        }
        return tripList;
}

private void validate(User loggedUser) throws UserNotLoggedInException {
        if (loggedUser == null) throw new UserNotLoggedInException();
}

功能羡慕

当一个类从另一个类获取数据以便对该数据进行某种计算或比较时,通常意味着客户端类羡慕另一个类。 这被称为Feature Envy(代码嗅觉) ,它在长方法中很常见,在旧代码中无处不在。 在OO中,数据和对该数据的操作应在同一对象上。

因此,查看上面的代码,很明显,确定用户是否与另一个用户成为朋友的整个过程不属于TripService类。 让我们将其移至User类。 首先进行单元测试:

@Test public void
shouldReturnTrueWhenUsersAreFriends() throws Exception {
        User John = new User();
        User Bob = new User();

        John.addFriend(Bob);

        assertTrue(John.isFriendsWith(Bob));
}

现在,让我们将代码移至User类。 在这里,我们可以更好地使用Java集合API,并一起删除整个for循环和isFriend标志。

public class User {

        ...

        private List<User> friends = new ArrayList<User>();

        public void addFriend(User user) {
               friends.add(user);
        }

        public boolean isFriendsWith(User friend) {
               return friends.contains(friend);
        }

        ...
}

经过一些重构步骤,这是TripService中的新代码

public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
        User loggedUser = loggedUser();
        validate(loggedUser);
        return (user.isFriendsWith(loggedUser))
                         ? findTripsByUser(user)
                         : new ArrayList<Trip>();
}

对。 这已经好多了,但还不够好。

层和依存关系

为了隔离依赖关系并测试类,我们中的某些人可能仍然对我们在第一部分中创建的受保护方法感到恼火。 这样的更改是临时的,也就是说,它们已经完成,因此我们可以对整个方法进行单元测试。 一旦测试完该方法后,就可以开始重构并考虑可以注入的依赖项。

很多时候,我们会认为应该将依赖项注入到类中。 听起来很明显。 TripService应该收到UserSession的实例。 真?

TripService是一项服务。 这意味着它驻留在服务层中。 UserSession知道有关登录用户和会话的信息。 它可能与MVC框架和/或HttpSession等通信。TripService是否应依赖于此类(即使它是接口而不是Singleton)? 用户是否已登录的整个检查可能应该由控制器或任何客户端类来完成。 为了不做太多更改(暂时),我将使TripService将已登录的用户作为参数,并完全删除对UserSession的依赖。 我需要做一些小的更改,并在测试中进行清理。

命名

不,很遗憾,我们还没有完成。 无论如何,此代码做什么? 朋友回程。 查看方法和参数的名称,甚至是类名称,都无法得知。 在TripService的公共界面中看不到“朋友”一词。 我们也需要改变它。

所以这是最终代码:

public class TripService {

      public List<Trip> getFriendTrips(User loggedUser, User friend) 
                                       throws   UserNotLoggedInException {
                      validate(loggedUser);
                      return (friend.isFriendsWith(loggedUser))
                                      ? findTripsForFriend(friend)
                                      : new ArrayList<Trip>();
      }

      private void validate(User loggedUser) throws UserNotLoggedInException {
                if (loggedUser == null) throw new UserNotLoggedInException();
      }

      protected List<Trip> findTripsForFriend(User friend) {
                 return TripDAO.findTripsByUser(friend);
      }
}

更好,不是吗? 对于其他受保护的方法,还有TripDAO静态调用等,我们仍然存在问题。但是,我将在最后一篇文章中留下有关如何删除对静态方法的依赖关系的文章。 我现在暂存重构。 我们不能在一天内重构整个系统,对吗? 我们仍然需要提供一些功能。 :-)

结论

这只是一个玩具示例,甚至可能没有任何意义。 但是,它代表了我们在使用遗留(现有)代码时发现的许多问题。 如此惊人的代码片段中能找到多少问题,真是令人惊讶。 现在想象一下所有具有数百行(甚至数千行)的类和方法。

我们需要继续毫不留情地重构代码,以免我们再也无法理解它了,并且由于我们无法足够快地调整软件,整个业务开始放缓

重构不仅仅涉及提取方法或对逻辑进行一些调整。 我们需要考虑依赖关系,每个类和方法应该承担的责任,体系结构层,应用程序的设计以及我们为每个类,方法,参数和变量指定的名称。 我们应该尝试在代码中表达业务领域。

我们应该将代码库当作一个大花园来对待。 如果我们希望它令人愉悦且易于维护,就需要不断地照顾它。

如果您想尝试一下此代码或查找有关实现的更多详细信息,请检查: https : //github.com/sandromancuso/testing_legacy_code

参考: 测试遗留物:来自Crafts Software博客的JCG合作伙伴 Sandro Mancuso的硬连接依赖性(第1部分和第2部分)


翻译自: https://www.javacodegeeks.com/2012/06/testing-legacy-code-hard-wired.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值