测试驱动开发书籍_图书管理员:测试驱动开发简介

测试驱动开发书籍

这将是一系列围绕单元测试的文章,在这些文章中,我将通过示例并探讨该技术的各个方面。 这是第一期。

与本文相关的代码可以在GitHub上找到 。 将来和过去的文章可以在The Librarian Archive中找到。

我将尝试对具有书籍和会员资格的Library模块实施一些要求,并随着测试的进行而扩展我们以测试驱动风格(“ TDD”)编写的所有代码 。 我对过程有一些想法,展示一些重构,并给出一些使用IDE的提示。

本文的级别适用于希望扩展测试范围的初级开发人员。

tdd

那里有很多信息来描述什么是TDD或测试驱动开发 ,红绿重构周期等,因此在这里我不会过多地介绍细节。 有关更多背景信息,请参见最后的参考。

相反,就开始吧!

我们从带有以下build.gradle文件的Gradle项目开始:

apply plugin: 'java'

repositories {
    jcenter()
}

dependencies {
    testCompile 'junit:junit:4.12'
}

这说明我们在一个干净的Java项目中,可以在src/test/java终止我们的测试,而在src/main/java终止源,并且我们依赖于JUnit(我们选择的测试框架)。 我可以选择TestNG或Spock,但这是另一篇文章的主题。

微故事中的第一个故事

我们正在通过测试驱动代码,并且正在根据需求驱动测试 。 那就是我们要重复的循环。

如果我们处于敏捷项目中,那么需求可能会以用户故事的形式出现,例如:

作为图书馆员,
我希望喜欢书的人成为图书馆的成员
这样我以后可以借书给他们

我们可以在此处确定一些概念:图书馆,人员,(成为会员),(借出)书籍。 让我们集中精力看一下我们的核心概念:图书馆。

因此,您可能很想进入并创建例如Library类和代码,但是我们不会这样做!

进行失败的测试

我们将从失败的测试开始。

package example;

import static org.junit.Assert.*;

import org.junit.Test;

public class LibraryTest {

    @Test
    public void test() {
        fail("Not yet implemented");
    }

}

一个名为test()公共方法,带有JUnit的@Test注释。 您可以在此处看到静态导入的方法fail()这在我们运行IDE或通过Gradle运行LibraryTest类时给我们带来了坚实的失败。

java.lang.AssertionError: Not yet implemented
    at org.junit.Assert.fail(Assert.java:88)
    at example.LibraryTest.test(LibraryTest.java:11)
    <snip>

意图透露名称

我们将把该方法重命名为一个更合适的测试名称,以显示我们第一个测试的意图 ,即测试成员应该能够注册

让我们shouldRegisterMembers()

package example;

import static org.junit.Assert.*;

import org.junit.Test;

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {
        fail("Not yet implemented");
    }

}

(是的,您可以运行它,但是它仍然会失败)

我没有任何要调用的代码

好吧 我们将改变它。

通过一系列执行良好的重构 (希望由您的IDE执行),我们将创建足够的代码来通过测试 。 这样,我们将创建我们的生产代码; Library类的代码尚不存在 。 现在的规则是:如果没有测试需要一段生产代码, 我们将不会编写它

我们需要一个Library类来注册其成员。 所以我现在写下了我唯一需要的代码。

package example;

import static org.junit.Assert.*;

import org.junit.Test;

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {

        // given
        Library library = new Library();
    }

}

如果您在IDE中,则会发出类似“无法将库解析为类型”之类的信号。 是的,这就是您友好的编译器是乐于助人的伙伴。 您需要通过创建类来解决此问题,或者让IDE为您创建它。

在例如Eclipse中,您可以选择一个称为“创建类库”的快速修复

使用IDE,卢克!

对于现代IDE而言,执行代码修改(例如创建缺少的类或方法)是不费吹灰之力的, 我强烈建议您始终将它们用于此类任务

我们新创建的Library看起来像

package example;

public class Library {
}

我们需要编译测试类并运行测试。 并通过。

仅仅实例化一个new Library测试(什么都不做)还没有增加价值。 这是必需的,因为我们需要根据用户故事(“注册成员”)创建逻辑。

有什么更好的方式来表示此方法调用: registerMember ? 我们对成员的了解还不多,但是现在我给他或她起一个名字 -一个简单的String可以用来识别成员。

稍后,我需要与我的图书馆员交谈,以阐明我们希望成员拥有的所有属性

因此,我们正在注册一个成员,并提供示例值“ Ted”。 现在的代码将如下所示:

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {

        // given
        Library library = new Library();

        // when
        library.registerMember("Ted");
    }

}

LibraryTest再次不再编译 。 编译器会抱怨:

The method registerMember(String) is undefined for the type Library

创建缺少的方法

使用IDE在Library类中创建此方法。 没什么可看的吗? 现在,我们将做一些新的事情: 向其中添加一些Javadoc,描述其功能

仍然不多,但是LibraryTest 再次编译

package example;

public class Library {

    /**
     * Registers a new member using provided name.
     * 
     * @param name
     *            The name of the member
     */
    public void registerMember(String name) {
    }

}

测试也通过了

由于我们仍然不确定我们已经实现了逻辑(因为我们还没有真正实现任何东西),所以让我们以某种方式设计事物,如果注册成员成功,那么库将为我们提供全新的,完整的信息。 Member回来了。

我们按以下方式调整测试:让registerMember方法返回成功创建的Member ,因此我们可以检查该成员的名称是否等于我们提供的名称。

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {

        // given
        Library library = new Library();

        // when
        Member newMember = library.registerMember("Ted");

        // then check for member's name to be same
    }

}

没错,此刻的registerMember方法返回void如果我们想要一个Member我们将不得不更改该方法的返回类型以使测试再次编译。 为此,我们首先必须创建一个Member类,因为该类也不存在。

public class Member {

}
public class Library {

    /**
     * Registers a new member using provided name.
     * 
     * @param name
     *            The name of the member
     * @return 
     */
    public Member registerMember(String name) {
    }

}

在测试的驱动下,我们是否真的可以继续更改并创建类似的东西?

是。

差不多了。 如果我们要检查我们可以使用Hamcrest成员的名字匹配器equalTo并且is一个尚未将要国产 getName() -这个名字“泰德”从与一个输入比较Member

package example;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {

        // given
        Library library = new Library();

        // when
        Member newMember = library.registerMember("Ted");

        // then
        assertThat(newMember.getName(), is(equalTo("Ted")));
    }

}

现在我们需要通过什么测试?

  • 要进行编译Member类将需要一个名为getName()的吸气剂。 按照惯例,但代码也需要我们将其放入registerMember ,使name为最终属性(不可变,没有设置器),我们将由构造方法对其进行初始化。 执行双重打击 ,并这样做:
public class Member {

    private final String name;

    public Member(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}
  • 创建一个Member并返回它。
public class Library {

    /**
     * Registers a new member using provided name.
     * 
     * @param name
     *            The name of the member
     * @return registered member
     */
    public Member registerMember(String name) {
        return new Member(name);
    }

}

! 测试通过。

看来我们已经按照用户故事满足了我们的要求,

作为图书馆员,
我希望喜欢书的人成为图书馆的成员
这样我以后可以借书给他们

满足要求了吗?

作为一个小小的旁注:我正在看这个故事,并且看到了“成员”,即复数形式。 由于我的测试仅测试一名成员的库,因此我正在努力加强工作,并测试更多成员。

测试的最小成员注册数量-我需要对支持“成员” (复数)的图书馆有足够的信心-是两个 ,对吧?

现在稍微修改了测试以包括Ted和Bob。

package example;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {

        // given
        Library library = new Library();

        // when
        Member newMember1 = library.registerMember("Ted");
        Member newMember2 = library.registerMember("Bob");        

        // then
        assertThat(newMember1.getName(), is(equalTo("Ted")));
        assertThat(newMember2.getName(), is(equalTo("Bob")));
    }

}

测试通过。

第二个故事–更快

让我们实现第二个用户故事。 它是这样的:

作为会计师,
我希望一个人只能注册一次
这样我就不会再为同一个人拥有多个会员资格

当您看时,用代码实现的第一个用户故事并不是什么大问题。 我们没有镀金的东西,我们没有任何未经测试的东西。 您知道进行失败测试,​​修复和重复的过程。

设计位

让我们创建一个名为shouldNotRegisterAgainWhenAlreadyMember的新测试–聪明吧?

package example;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() { ... }

    @Test
    public void shouldNotRegisterAgainWhenAlreadyMember() {

        // given
        Library library = new Library();
        library.registerMember("Ted");

        // when we register with same name
        library.registerMember("Ted");

        // then we should see it fail somehow
    }
}

这是进行一些设计的地方。我们如何让代码告诉我们已经注册了相同名称的成员? 如果通常不可能做到这一点,但是我们仍然必须处理这种情况,则可以使用Java的异常机制。

我们将创建一个AlreadyMemberException方法registerMember()可以抛出该异常以指示此异常事件。

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() { ... }

    @Test
    public void shouldNotRegisterAgainWhenAlreadyMember() {

        // given
        Library library = new Library();
        library.registerMember("Ted");

        // when we register with same name
        try {
            library.registerMember("Ted");
            fail("should not have registered Ted twice");
        } catch (AlreadyMemberException e) {
            // success!
        }

    }
}

怎么了?

  • 作为测试设置的一部分( //given ),该库以名称为“ Ted”的现有成员开始。
  • 当告知图书馆(“不要询问”)以相同的名称“ Ted”注册另一个成员时,我们希望抛出AlreadyMemberExceptionLibrary不应允许多个具有相同名称的成员
  • 如果registerMember("Ted")没有引发异常,我们将对fail()测试fail()
  • 可能会有更优雅的方式来期望会抛出某些异常,但是我们不想超越自己

目前测试失败-由于我们尚未更新Library因此不会引发任何异常。

现在开始吧。

该代码现在应该以某种方式在内部跟踪成员,否则它将无法记住在两次调用之间注册的成员

最简单的解决方案(KISS)为此使用了Collections Framework中的数据结构。 任何Collection (例如ArrayList )都具有诸如contains (用于检查是否存在)和add )之类的方法,以满足我们的所有需求:

  • 检查会员
  • 添加成员

我们的解决方案如下所示:

package example;

import java.util.ArrayList;
import java.util.Collection;

public class Library {

    private final Collection<Member> members = new ArrayList<>();

    /**
     * Registers a new member using provided name.
     * 
     * @param name
     *            The name of the member
     * @return registered member
     */
    public Member registerMember(String name) {

        Member newMember = new Member(name);

        if (members.contains(newMember)) {
            throw new AlreadyMemberException();
        }

        members.add(newMember);
        return newMember;
    }

}

运行我们的测试方法,然后… 仍然会失败 。 WAT?

了解你的框架

没有人抛出异常 ,但是代码很简单,可以期望containsadd应该可以正常工作。 这是测试本身的缺陷吗?

不,人们必须知道何时将类似Member对象放入Collection并且我们希望Collection检查是否相等 (而非身份 ),我们需要在Member实现equals()hashCode()

您可以使用任何不错的IDE生成这些方法(或从辅助框架调用辅助方法来执行此操作),但是下面的代码(由Eclipse生成)就足够了:

package example;

public class Member {

    private final String name;

    public Member(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Member other = (Member) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

}

测试通过!

重构

现在我们已经有了一些测试范围,我们可以做到现在为止还没有做的事情: 重构

规则是:

  1. 让它起作用
  2. 变得更好(更快等)
  3. 使其可读(干燥,可维护等)

我们成功了。 通过重构过程(通常被称为“一系列保留小行为的转换”),我们可以解决剩下的两个问题:如果可能的话,使其“更好”并易于阅读。 这始终是可能的:多年来,我遇到的陷阱是人们可以重构到无限远–知道何时停止通常很棘手。

这些只是示例,但我们可以…

简化实施

如果我们转换为Collection类型,从而防止自身重复,那么我们可能可以使存在检查更加简单,摆脱contains并直接使用add的返回值。

我们可能可以用HashSet替换ArrayList ,如果成员集合不允许添加已经拥有的成员,则add将返回false 。 看起来像:

package example;

import java.util.Collection;
import java.util.HashSet;

public class Library {

    private final Collection<Member> members = new HashSet<>();

    /**
     * Registers a new member using provided name. [...] 
     */
    public Member registerMember(String name) {

        Member newMember = new Member(name);

        if (!members.add(newMember)) {
            throw new AlreadyMemberException();
        }

        return newMember;
    }

}

简化测试

哈,您认为只有实现才需要工作? 不,测试也是测试修复重构的每个迭代都可以完善的条件。

例如,如果我们查看LibraryTest中每个测试方法中相同的部分,则是new Library()的实例化。

public class LibraryTest {

    @Test
    public void shouldRegisterMembers() {

        // given
        Library library = new Library();
        ...
    }

    @Test
    public void shouldNotRegisterAgainWhenAlreadyMember() {

        // given
        Library library = new Library();
        ...

    }
}

我们可以应用称为“ 将局部变量转换为字段”的重构并在每次测试之前使用JUnit的@Before注释初始化我们的字段。

public class LibraryTest {

    private Library library;

    @Before
    public void setUp() {
        library = new Library();
    }

    @Test
    public void shouldRegisterMembers() {

        // when
        Member newMember1 = library.registerMember("Ted");
        Member newMember2 = library.registerMember("Bob");        

        ...
    }

    @Test
    public void shouldNotRegisterAgainWhenAlreadyMember() {

        // given
        library.registerMember("Ted");

        ...

    }
}

测试通过!

(您可能想知道我什么时候才能解决它:-))
最后但并非最不重要的一点是-有一种方法可以简化对JUnit的某些异常ExpectedExceptionExpectedException规则-确实摆脱了我们上一次测试的混乱。

package example;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class LibraryTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    private Library library;

    @Before
    public void setUp() {
        library = new Library();
    }

    @Test
    public void shouldRegisterMembers() { ... }

    @Test
    public void shouldNotRegisterAgainWhenAlreadyMember() {

        // given
        library.registerMember("Ted");

        // fail when we register with same name
        thrown.expect(AlreadyMemberException.class);
        library.registerMember("Ted");

    }
}

测试通过!

结论

我同意许多年前James Shore在一篇文章(“ TDD如何影响设计”)中得出的一些结论:TDD可以导致更好的设计,TDD可以导致更差的设计。 TDD视角只是测试领域的众多视角之一,并且是任何人的工具带中值得欢迎的补充。 我想相信我们还没有创建任何未经测试的生产代码,但是我还没有运行任何代码覆盖率工具来验证是否覆盖了所有路径–但这不是现在的目标无论哪种方式

我希望这篇文章能简要介绍一下TDD循环如何工作以及如何通过我们的测试来指导类的设计,并最终带来回归测试套件,这是一个很大的副作用。

参考资料

翻译自: https://www.javacodegeeks.com/2016/06/librarian-introduction-test-driven-development.html

测试驱动开发书籍

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值