如何对具有数据库访问的方法进行单元测试?

6
我已经在单元测试方面遇到了一些困难,我正在尝试在我目前正在工作的小项目中学习它,我遇到了这两个问题,希望你能帮助我解决:
1- 我的项目是一个MVC项目。我的单元测试应该从哪个级别开始?他们应该只关注业务层吗?他们还应该测试控制器上的操作吗?
2- 我有一个方法来验证用户名格式,然后访问数据库检查它是否可用。返回值是一个布尔值,表示此用户名是否可用。 对于这样的方法,是否应该创建一个单元测试? 我想测试格式验证,但是如何在不查询数据库的情况下进行检查?另外,如果格式正确,但用户名已被使用,我将得到一个false值,但验证仍然有效。我可以将这种方法解耦,但是只有在格式正确时才应该进行DB验证,因此它们应该以某种方式相互联系。 具有单元测试知识的人会如何解决这个问题?或者如何重构此方法以便能够测试它? 我可以为DB访问创建一个存根,但是如何在用户测试时将其附加到我的项目中,在本地运行时将其分离?
谢谢!
2个回答

3
在您的具体情况下,您可以做的一件容易的事是将验证方法分解为3个不同的方法:一个用于检查格式,一个用于检查数据库可用性,以及一个将它们都联系起来的方法。这将允许您单独测试每个子函数。
在更复杂的情况下,其他技术可能会有用。本质上,这就是依赖注入控制反转派上用场的地方(不幸的是,这些短语对不同的人意味着不同的事情,但了解基本思想通常是一个好的开始)。
您的目标应该是将“检查此用户名是否可用”的概念与检查数据库的实现分离开来。
因此,而不是这样:
public class Validation
{
    public bool CheckUsername(string username)
    {
        bool isFormatValid = IsFormatValid(username);
        return isFormatValid && DB.CheckUsernameAvailability(username);
    }
}

你可以像这样做:
public class Validation
{
    public bool CheckUsername(string username,
                              IUsernameAvailabilityChecker checker)
    {
        bool isFormatValid = IsFormatValid(username);
        return isFormatValid && checker.CheckUsernameAvailability(username);
    }
}

然后,从您的单元测试代码中,您可以创建一个自定义的IUsernameAvailabilityChecker,用于测试目的。另一方面,实际的生产代码可以使用不同的IUsernameAvailabilityChecker实现来实际查询数据库。

请记住,有许多,许多解决这种测试问题的技术,我给出的示例是简单和人为的。


谢谢!有没有办法根据是单元测试还是在我的机器上真正运行来自动更改实现(模拟/非模拟)? - JSBach
1
@Oscar:当然。依赖注入的重点在于调用代码决定你要做什么。当你编写单元测试时,将一个模拟的IUsernameAvailabilityChecker传递给函数。当你实际从MVC控制器(或其他地方)调用它时,创建并传递一个真正的IUsernameAvailabilityChecker,它访问数据库。 - voithos

1

使用模拟可以对外部服务进行测试。如果您已经使用接口完成了良好的工作,那么很容易模拟应用程序的各个部分。这些模拟可以注入到您的单元中,并像通常处理它一样使用。

您应该尽早开始单元测试。如果您的应用程序尚未完成或需要测试的代码不存在,则仍然可以针对某些可以模拟的接口进行测试。

顺便说一下:单元测试是关于测试行为的,不是发现错误的有效方法。您将通过测试发现错误,但这不应该是您的目标。

例如:

interface UserService {
    public void setUserRepository(UserRepository userRepository);
    public boolean isUsernameAvailable(String username);
}


class MyUserService implements UserService {
    private UserRepository userRepository;

    public vois setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public boolean isUsernameAvailable(String username) {
        return userRepository.checkUsernameAvailability(username);
    }
}


interface UserRepository {
    public boolean checkUsernameAvailability(String username);
}

// The mock used for testing
class MockUserRepository {
    public boolean checkUsernameAvailability(String username) {
        if ("john".equals(username)) {
            return false;
        }
        return true;
    }
}

class MyUnitTest {
    public void testIfUserNotAvailable() {
        UserService service = new UserService();    
        service.setUserRepository(new MockUserRepository);

        assertFalse(service.isUsernameAvailable('john')); // yep, it's in use
    }

    public void testIfUserAvailable() {
        UserService service = new UserService();    
        service.setUserRepository(new MockUserRepository);

        assertTrue(service.isUsernameAvailable('mary')); // yep, is available
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接