编程人生 软件匠艺,这次我认真了 2

jinhuisheng · May 07, 2021 · Last by jinhuisheng replied at May 13, 2021 · 301 hits
Topic has been selected as the excellent topic by the admin.

一、缘由

继上篇 软件匠艺,这次我认真了之后,差不多 4 个月时间过去了,也对 tdd 有了更深刻的认识,也做了更多的练习,在工作中也有了更多的实践,遇到了很多问题,也解决的很多问题,懂了很多,也收获了很多。

也该对自己这段时间的成长做下总结了!

现在真正的相信了:

任务分解真的可以把步子拆得很小呀

小步快走,真的可以倒着推,一步一步把业务逻辑驱动出来呀

先写测试,真的可以一步一步把 resource、service、domain、repository .....接口实现逻辑完全驱动出来呀

这些都是真的可以做到的呀,熊节老师、小波老师......他们说的可都是真的呀😀 !!!

二、工作中的应用

之前还是部分使用 tdd 的方式,现在项目中也基本上完全使用的 TDD 的方式了,同时配置了质量门禁。

其中一个项目测试覆盖率达到 80%

还有一个做了一年多的项目

之前的测试代码很少,也给它加了质量门禁,以后提交代码的测试覆盖率不能低于之前的,新增功能要先写测试,改到之前的逻辑,再去加测试覆盖它,再去修改,可能几个月后就会有很大的改进。

当然还有很多需要改进的地方,比如:测试写得不够好,代码不够整洁,关注圈复杂度,CI 效率的问题。。。

需要继续去学习、改进,发现问题、解决问题,不断变得更好!

三、收获

当然这些收获都是这个阶段的认识,可能过段时间就会发现其中有些认知是错误的,没关系,本来这就是一个不断改进的过程。

1、TDD 要灵活

特别是刚开始写测试的时候,都是比较死板的去做,但这就是一个过程,练得多了,才能理解。

外层测试保障下,不需要对所有的单元都加测试

如果某个分支,外层测试已覆盖,就不需要对它再加测试,可以对新的分支逻辑单独加测试。

就好比接口开发中,如果逻辑很简单,一个接口实现过程连一个 if -else 逻辑都没有,一个接口测试就已经把从 resource 到 service,到 domain,到 repository 的代码都驱动出来了,就不需要去写单元测试了,接口测试已经完全覆盖了。 如果还有分支逻辑时,我们可以在具体分支变动逻辑的类的行为上增加单元测试去覆盖就好了。

比如,在 cyber-dojo 上的 kata bowling-game

@Test
void should_be_300_when_frames_are_strike() {
    BowlingGame bowlingGame = new BowlingGame("X|X|X|X|X|X|X|X|X|X||XX");
    assertThat(bowlingGame.score()).isEqualTo(300);
}

通过 should_be_300_when_frames_are_strike 测试,已经驱动出了 Frame 对"X"的解析,当实现下一个测试时:

@Test
void should_be_90_when_frames_miss_one_ball() {
    BowlingGame bowlingGame = new BowlingGame("9-|9-|9-|9-|9-|9-|9-|9-|9-|9-||");
    assertThat(bowlingGame.score()).isEqualTo(90);
}

发现步子太大,需要先实现 Fame 对"9-"的解析,这个时候只需要对 Frame 针对参数"9-"的解析加测试驱动就好:

@Test
void should_new_miss_one_ball_frame_success() {
    Frame frame = FrameFactory.create("9-");
    assertThat(frame.getBowlingPins()).isEqualTo(9);
    assertThat(frame.getFirstPinFall()).isEqualTo(9);
    assertThat(frame.getSecondPinFall()).isEqualTo(0);
}

而不是再把 Frame 对参数"X"的解析再加一个测试来实现,因为对参数"X"的解析是否正确,外层测试 should_be_300_when_frames_are_strike 已经覆盖了,没必要再去实现一次。

有时可以先写实现再增加测试覆盖

探索性的,或者造数据麻烦的,都可以先去实现,再写测试覆盖。

比如一个爬虫程序,新增爬虫平台的时候,因为爬取到的数据还需要中间的处理,比如图片的转存替换、css 的调整。

我是先写的爬虫逻辑,运行起来去不断测试爬取的逻辑、得到的数据是否正确,然后拿到这次爬取的结果当作测试数据,再加一个集成测试来保证它的正确。

如果我是先写测试,再去实现,这个测试数据都很难去造出来,中间的图片转存、css 替换更是无法预先想清楚或者造出来,反而很麻烦,耽误了很多时间。

很多简单逻辑步子可以大一些

最开始练习 TDD 的时候,最大的一个困扰就是不会小步快走,比如:写接口的时候,先定义好了接口测试,然后就直接去实现 service、domain、repository 了,因为最开始不会小步慢走,所以,就不知道这个过程如何驱动,很困惑,没有体会到驱动的感觉。

然后开始不断去练习小步,比如用 TPP 的方式去练习 kata,工作中不断实践,才知道如何小步。

比如,我要实现一个保存学校信息的接口,如果要小步驱动,步骤如下:

(1)、身份认证 → 驱动出接口和身份认证

@Test
void create_school_success() throws Exception {
    mockMvc.perform(post("/api/schools")
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isUnauthorized());
}
@RestController
@RequestMapping("/api/schools")
public class SchoolResource {
    @PostMapping
    @Secured({AuthoritiesConstants.ADMIN})
    public void createSchool() {

    }
}

(2)、 修改 expect status=200

@AutoConfigureMockMvc
@SpringBootTest(classes = App.class)
public class SchoolResourceIT {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void create_school_success() throws Exception {
        mockMvc.perform(post("/api/schools")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());


→ 驱动出测试调用接口携带身份

@AutoConfigureMockMvc
@WithMockUser(authorities = {AuthoritiesConstants.ADMIN})
@SpringBootTest(classes = App.class)
public class SchoolResourceIT {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(authorities = {AuthoritiesConstants.ADMIN})
    void create_school_success() throws Exception {
        mockMvc.perform(post("/api/schools")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }
}

(3)、定义校验结果(如果多的话,先校验一部分,也相当于拆成小步)

@Test
void create_school_success() throws Exception {
    mockMvc.perform(post("/api/schools")
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk());

    School school = schoolRepository.findAll().get(0);
    assertThat(school.getLogoUrl()).isEqualTo("logo url");
    assertThat(school.getBaseInfos()).usingRecursiveFieldByFieldElementComparator()
            .usingElementComparatorOnFields(
                    SchoolBaseInfo.Fields.address,
                    SchoolBaseInfo.Fields.description,
                    SchoolBaseInfo.Fields.lang,
                    SchoolBaseInfo.Fields.name)
            .containsExactlyInAnyOrderElementsOf(Arrays.asList(
            new SchoolBaseInfo("郑大", "经济开发区", "郑州大学", "zh-CN"),
            new SchoolBaseInfo("Zhengzhou university", "Economic development zone"
                    , "Zhengzhou university", "en-US")
            ));

}

→ 驱动出校验结果的保存逻辑

@RestController
@RequestMapping("/api/schools")
public class SchoolResource {
    @Autowired
    SchoolRepository schoolRepository;
    @PostMapping
    @Secured({AuthoritiesConstants.ADMIN})
    public void createSchool() {
        schoolRepository.save(new School("logoUrl"));
    }
}


@Getter
@Entity
@Table(name = "school")
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class School extends AbstractAuditingEntity {
    @Id
    @Column(length = 36, unique = true, nullable = false)
    private String id = UUID.randomUUID().toString();

    private String logoUrl;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name = "school_id")
    private List<SchoolBaseInfo> baseInfos;

    public School(String logoUrl) {
        this.logoUrl = logoUrl;
        this.baseInfos = Arrays.asList(
                new SchoolBaseInfo("郑大", "经济开发区", "郑州大学", "zh-CN"),
                new SchoolBaseInfo("Zhengzhou university", "Economic development zone"
                        , "Zhengzhou university", "en-US")
        );
    }

}


(4)、测试保证下,重构,消除重复 (常量就是重复)

→驱动参数,保存时写死的数据是从哪里来的

@PostMapping
@Secured({AuthoritiesConstants.ADMIN})
public void createSchool() {
    CreateSchoolCommand createSchoolCommand = new CreateSchoolCommand();
    createSchoolCommand.setLogoUrl("logoUrl");
    createSchoolCommand.setBaseInfos(Arrays.asList(
            new SchoolBaseInfo("郑大", "经济开发区", "郑州大学", "zh-CN"),
            new SchoolBaseInfo("Zhengzhou university", "Economic development zone"
                    , "Zhengzhou university", "en-US")
    ));
    schoolRepository.save(createSchoolCommand.createSchool());
}



@Getter
@Setter
public class CreateSchoolCommand {
    private String logoUrl;
    private List<CreateSchoolBaseInfoCommand> baseInfos;

    public School createSchool() {
        return new School("logoUrl", Arrays.asList(
                new SchoolBaseInfo("郑大", "经济开发区", "郑州大学", "zh-CN"),
                new SchoolBaseInfo("Zhengzhou university", "Economic development zone"
                        , "Zhengzhou university", "en-US")
        ));
    }
}

再进一步,将 command 提取到参数中,在测试中传参

@PostMapping
@Secured({AuthoritiesConstants.ADMIN})
public void createSchool(CreateSchoolCommand createSchoolCommand) {
    schoolRepository.save(createSchoolCommand.createSchool());
}

@Getter
@Setter
public class CreateSchoolCommand {
    private String logoUrl;
    private List<CreateSchoolBaseInfoCommand> baseInfos;

    public School createSchool() {
        return new School(logoUrl, createBaseInfos(baseInfos));
    }

    private List<SchoolBaseInfo> createBaseInfos(List<CreateSchoolBaseInfoCommand> baseInfos) {
        return baseInfos.stream().map(this::createBaseInfo).collect(Collectors.toList());
    }

    private SchoolBaseInfo createBaseInfo(CreateSchoolBaseInfoCommand baseInfo) {
        return new SchoolBaseInfo(baseInfo.getName(), baseInfo.getAddress(), baseInfo.getDescription(), baseInfo.getLang());
    }
}

@Test
void create_school_success() throws Exception {
    CreateSchoolCommand createSchoolCommand = new CreateSchoolCommand();
    createSchoolCommand.setLogoUrl("logoUrl");
    createSchoolCommand.setBaseInfos(Arrays.asList(
            new CreateSchoolBaseInfoCommand("郑大", "经济开发区", "郑州大学", "zh-CN"),
            new CreateSchoolBaseInfoCommand("Zhengzhou university", "Economic development zone"
                    , "Zhengzhou university", "en-US")
    ));
    mockMvc.perform(post("/api/schools")
            .contentType(MediaType.APPLICATION_JSON)
            .content(TestUtil.convertObjectToJsonBytes(createSchoolCommand)))
            .andExpect(status().isOk());

    School school = schoolRepository.findAll().get(0);
    assertThat(school.getLogoUrl()).isEqualTo("logoUrl");
    assertThat(school.getBaseInfos()).usingRecursiveFieldByFieldElementComparator()
            .usingElementComparatorOnFields(
                    SchoolBaseInfo.Fields.address,
                    SchoolBaseInfo.Fields.description,
                    SchoolBaseInfo.Fields.lang,
                    SchoolBaseInfo.Fields.name)
            .containsExactlyInAnyOrderElementsOf(Arrays.asList(
            new SchoolBaseInfo("郑大", "经济开发区", "郑州大学", "zh-CN"),
            new SchoolBaseInfo("Zhengzhou university", "Economic development zone"
                    , "Zhengzhou university", "en-US")
            ));
}

(5)、 如果第(3)步(如果多的话,先校验一部分,也相当于拆成小步),只做了部分结果的断言,现在增加其他的断言

(6)、重构代码(消除重复、表达意图)

这样来写,每一步驱动的内容很清晰,每一步驱动了一点,也能快速反馈,不断小步实现接口逻辑。

但学会了小步之后,你就有了小步的能力,就不需要每写一个接口都要非常小步地去实现的,这样很浪费时间。

可以大步一点去做,特别是简单的接口,比如直接复制上一个测试代码,然后改一下,直接定义所有断言,直接去实现 resource、service、domain、repository。。。

当不知道怎么做的时候再去小步快走地方式去驱动它,来帮助自己降低心智难度,快速反馈,倒着去推出来。

这样看起来,好像又回到了最开始学习 TDD 的时候了,但是这个时候你已经知道了为什么要这样,你可以去控制、选择如何去实现它了。

偶尔 debug 快速定位问题

虽然很多人说,用了 TDD,很多人说都不需要调试代码,甚至很多人认为调试代码是初级程序员才去做的事,但这个也是站在不同 TDD 能力的人来说的。但谁会认为自己是初级程序员呢,刚开始我对这个就会有误解,甚至排斥去 debug,宁愿加 log,也不愿去调试代码,反而浪费了很多时间。

加了单测后,确实,因为粒度比较小,测试不通过,很多时候都可以直接从失败的结果中判断出来错误的原因,但有些时候对任务的分解粒度不够或者不对,还是不能直接看出来,这个时候,倒不如直接 debug 一下,快速定位问题,并修复它。

而且,idea 本身提供的调试功能是非常强大的。

比如,我要调试一个未通过的测试的失败原因:

  • control + command + 8 直接在指定行加入断点

  • control +d 调试状态运行

  • alt + Command + R 直接运行到下一个断点

  • shift + command + N Step over 跳过当前行运行

  • shift + command + I Step into 进入下一层方法运行

  • control + / Evaluate 运行当前选中内容,并显示结果

这个 debug 过程,完全可以通过 idea 快捷键(部分快捷键自定义了按键)操作来完成。

这样可以非常快的定位问题,并修复它。

2、及时重构

就算学会了先写测试,如果不及时重构代码,TDD 也是坚持不下去的。

测试代码的重构

之前也是因为测试代码重复性太高,不知道测试代码也是需要重构的,后来发现,测试准备数据一大堆重复,导致后边不想再写测试了,因为测试代码改起来太费劲了。

也是来来回回放弃,又拾起来的尝试,反思,开始认识到测试代码维护的重要性。

测试代码中一个对象的创建,多处使用,也是需要消除重复的。

比如测试数据的准备:

(1)、可以用工厂类封装对象的创建;

(2)、准备数据比较复杂,可以直接用 json 字符串文件去提供;

提升重构能力

要重构代码,就要能识别出坏味道,有一些面向对象的设计能力,但这个是需要不断地去练习,才能发现识别出坏味道的。

要不断地去读《重构》,不断地去练习,比如练 kata,本来做完一个 kata 需要 2 个小时,把它练到半个小时,20 分钟。那就完全不一样了,而且真的是可以练到的。 如果把多个 kata 练到非常短的时间,练到肌肉记忆,可能重构可能真的就像熊节老师说的” 无脑的模式匹配 “那样了。

重构的时机

(1)、测试通过

测试通过后,就应该去消除重复、消除坏味道

这个时候的重构是最简单的,成本是最低的,时间长了,重构的成本就会越大,拖的越久,越不好搞。

(2)、添加新功能的时候

添加新功能时,会发现要调整现有代码结构,如果直接去实现,就破坏了之前的很多测试,这个时候就可以先重构现有代码,再去实现新功能。

比如:新增加测试,构造函数要多传一个参数进去,那这个时候,就可以对之前的代码进行重构,增加构造函数参数,并通过测试。这个时候再去新增功能。

3、简单设计

记得 2,3 年前上家公司,提交代码给架构师 review 的时候,架构师经常觉得我实现的逻辑太简单了,对我说,这个系统是要以后给很多公司用的,这些问题你都考虑了吗?这个后边会很复杂的,你想到了吗?

之前还有个领导给我们提需求的时候,总是说,这个需求以后一定会变,要留个口子,以后变得时候好扩展,更容易? 怎么留口子呢?加个字段?if-else 加个 else?

我总是一脸无奈,又无话可说,会觉得这是自己经验的问题,自己知道的太少了,可是我确实没想到呢,我去实现功能时也是根据当前提出的需求去实现的呀?以后会有很多地方要去用,要去怎么用,也没有人知道,让我怎么去考虑以后兼容的问题呀?

那我写出的代码,以后别人看时,会不会像下边这张图那样呢?

直到现在我才明白,这些很多时候都是过度设计,就算我没有相关的经验,我还是可以去做的,我只要保持代码的简单设计,以后需要什么,我就去改就好了,我为什么要为连需求怎么变都不知道的时候就得想清楚呢,代码应该是不断演化的,随着项目的深入,知识的不断理解,保持简单设计,需要的时候再去改变就好了。

简单设计原则:

  • 通过所有测试(Passes its tests)

  • 尽可能消除重复 (Minimizes duplication)

  • 尽可能清晰表达 (Maximizes clarity)

  • 更少代码元素 (Has fewer elements)

以上四个原则的重要程度依次降低

四、改变认知

1、不写测试,代码只会一直烂下去

通过之前 重构爬虫项目反思就是个例子,虽然平时写代码有注意代码的质量,写的方法长度也不长,会去用一些 idea 的重构快捷键做一些重构,但代码不是一次就能写好的,必须要随着功能的迭代不断的重构,也真的认识到,不写测试,没有测试保证,就不能进行重构,代码就不可能写好,它只会不断烂下去,越来越烂!

2、注重过程,让自己慢下来

如果你开发的时候,是想着先把功能实现了,再去写测试,那写完功能后,你也不会去写测试了。

同样地,就算你是用 tdd 的方式去开发一个功能,所有测试一旦都通过了,如果工期比较赶,你就可能不会去重构了,你就要赶着去上线,或者做下个功能了。

那如果我们去做这个功能的时候,放慢一些呢?这样,我们是不是会去把过程中发现的问题给解决了呢?

之前看小波老师直播,要部署一个服务,配置 nginx,在 ansible 里对 nginx 配置改来改去,花了很长时间,而不是直接去服务器上直接修改一下 nginx 配置,看起来这个过程慢了很多,但现在想想,如果当时直接改了,还会去思考如何解决 ansible 的配置问题吗,如果暂时凑合,以后还会信任 ansible 吗。

自己也发现工作中对很多问题的处理,包括重构,都是慢下来,注重过程的时候,才会去解决,或者做得更好的。

比如:

(1)、项目中经常会用到 findbyId,它的返回值是 Optional ,但项目中,很多时候访问它的时候,都是必定存在的,就像登录用户获取用户信息,还会不存在吗?那是不是还要去写不存在的逻辑呢?不然 sonar 就会有 bug。

managerRepository.findById(id).orElseThrow(() -> new BadRequestAlertException("数据不存在"))

那这样,很多对象的获取都要去实现这个逻辑,是不是很重复呢?

如果这个时候,慢下来一些,是不是可以提取一个 BaseRepository,和该方法的实现 BaseRepositoryImpl,并对这个逻辑加一个测试去覆盖,这样,如果是必须存在的对象获取,就直接调用这个方法,可能不存在的再去调用 findById(),是不是再也不用考虑这个问题了呢

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable>
    extends JpaRepository<T, ID> {
    T findExistOne(ID id);
}

@NoRepositoryBean
public class BaseRepositoryImpl<T, ID extends Serializable>
    extends SimpleJpaRepository<T, ID> implements BaseRepository<T, ID> {

    public BaseRepositoryImpl(JpaEntityInformation<T, ?>
                                  entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
    }

    @Override
    public T findExistOne(ID id) {
        return findById(id).orElseThrow(() -> new BadRequestAlertException("数据不存在"));
    }
}

@Test
void test_base_repository_extend_method_success() {
    Subject subject = subjectRepository.save(Subject.builder()
        .name("英语")
        .build());

    Subject one = subjectRepository.findExistOne(subject.getId());
    assertThat(one.getName()).isEqualTo(subject.getName());
}

@Test
void test_base_repository_extend_method_throw_exception() {
    Assertions.assertThrows(BadRequestAlertException.class,
        () -> subjectRepository.findExistOne("id"),
        "数据不存在");
}

(2)、项目中 service 会去加事务,如果忘了加,多个表的处理时,如果有报错,就会出现数据不完整的情况,发现这个问题时,就手动给他加上事务的注解,有些时候,还是会忘,那怎么处理这种情况呢?那要加集成测试测这个吗?要造一下它的报错吗?那是不是很多接口都要去写呢,是不是挺麻烦的呢,然后就暂时先不写了。

如果这个时候,慢下来一些,查些资料,好像可以用 ArchTest 来做这件事吧,写个测试校验一下所有的 Service 是不是都加了@Transactional的注解,如果有没加的,就报错,你再去加上,是不是以后再也不用去考虑这些问题了呢?

@Test
void should_all_services_have_transactional_annotations() {
    JavaClasses importedClasses = new ClassFileImporter()
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
        .importPackages("com.xxx.xxxx");

    noClasses()
        .that()
        .areAnnotatedWith(Service.class)
        .should()
        .notBeAnnotatedWith(Transactional.class)
        .check(importedClasses);
}

其实很多紧急的事情并没有那么紧急,多享受这个过程,体会它的乐趣。

而且慢下来,并不是让你很慢很慢,而是让你多注重过程!

当你真的慢下来去思考的时候,你真的会发现很多问题需要解决,才会提高的更快,代码写得更整洁!

3、工程师思维、职业修养

之前和小波老师一块结对直播的时候,我看到了工程师是如何消除潜在的时间浪费的,是会按正确的方式去开发的,对我非常有启发。

Bob 大叔的《代码整洁之道 - 程序员的职业修养》有说到:

选择那些你在危机时刻依然会遵守的纪律原则,并且在所有工作中都遵守这些纪律。

遵守这些纪律原则是避免陷入危机的最好途径。

当困难降临时,也不要改变行动。如果你遵守的纪律原则是工作的最佳方式,那么即使是在深度危机中,也要坚决秉持这些纪律原则。

TDD、整洁代码、简单设计、结对编程、用正确的方式做事、以工程师思想去要求自己,这就是工作的最佳方式。

我们是否遵守了这些纪律原则呢?

不然一到非常忙的时候,或者压力很大的时候时,就又变成本能反应了!

改变本能,坚守原则,我们才不会衡量利弊!!!

五、下一步计划

从软件开发的过程来看,需求分析、任务分解、单元测试、持续集成、快捷键的使用,看似各自独立,其实环环相扣!

如果需求分析错了,任务拆分、单元测试、持续集成。。。都无从谈起。

如果任务不会拆分,单元测试就不清晰明确,粒度就不好掌控。

如果没有单元测试,质量就无法保证,后续迭代无法保证,进度越来越慢!

如果没有持续集成、质量门禁,团队整体的代码质量就无法保证。

如果快捷键、自动生成代码、命令行工具使用,没有形成肌肉记忆,也就没有时间去做更多的思考,更高层级的设计。

。。。。。

如何让这个循环越来越流畅,越来越快,可能这才是程序员需要不断刻意练习,写出优雅代码,成为软件艺匠的关键。

所以,对不熟练的环节还需要不断练习,来加快这个循环。下一阶段希望去练习的内容:

1、BA 可视化需求分析

大锤老师的课程报了,但总是有别的事耽误了,可能这还不是目前工作中最大的痛点,最需要解决的。

希望下一阶段有更多的收获。

2、设计能力、面向对象能力

没有设计能力,可能只能倒着推,如果能画出 CRC 关系类图什么的,再去实现,设计是不是会更好呢

3、算法能力

感觉基础的算法能力,还是要有的,很多代码逻辑,如果会一些算法的话,也可以让代码更整洁一些的

就比如 fizzbuzz ,我最初就写不出这种:


private String parse() {
    String result = "";
    if (isRelatedTo(FIZZ_NUMBER)) {
        result += "Fizz";
    }
    if (isRelatedTo(BUZZ_NUMBER)) {
        result += "Buzz";
    }
    return result.isEmpty() ? valueOf(rawNumber) : result;
}

而是这种:

private String parse() {
    if (isRelatedTo(FIZZ_NUMBER) && isRelatedTo(BUZZ_NUMBER)) {
        return "FizzBuzz";
    }
    if (isRelatedTo(FIZZ_NUMBER)) {
        return "Fizz";
    }
    if (isRelatedTo(BUZZ_NUMBER)) {
        return "Buzz";
    }
    return valueOf(rawNumber);
}


这好像就是看你有没有算法的能力了吧,跟 TDD 感觉没有什么关系了,而且代码确实整洁了一些.

4、敏捷管理

还没有在工作中接触过真正的敏捷实践,希望去学习,并在团队中去实践

最后

相比年前,自我感觉已经进步了很多,但还有很多问题,还需要不断地练习,继续精进。

但心里已经很明确的一点就是,我已经上了 XP 这条船了,而且越来越有自信了,而且会在这条船上一直游下去,游得更远!!!

希望通过文章的形式来记录和督促我的成长,也希望能够帮助到遇到类似问题的一些小伙伴😁

「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」,让帮助程序员更加快乐高效地编程!

同感,Tdd 要灵活才能更加有效和增加效率,不过就像文中说的,这不是简单的省略步骤,而是一个经过一般过程后,返璞归真出来的结果,如此,tdd 可以在项目中事半功倍!

seabornlee mark as excellent topic. 12 May 11:59

如果我是先写测试,再去实现,这个测试数据都很难去造出来,中间的图片转存、css 替换更是无法预先想清楚或者造出来,反而很麻烦,耽误了很多时间。

我还是会先写测试,先做基础的断言,再逐渐完善断言。

Reply to seabornlee

哈哈,很有道理,非常对,这样更降低了难度,每次驱动一小步

  • 1、先写一个集成测试,验证它的结果可以得到内容,这个测试就通过了,也证明处理流程是有的。
  • 2、可以针对替换图片的逻辑加单元测试,驱动出来图片转存的逻辑;
  • 3、针对样式调整的逻辑加单元测试,驱动样式替换逻辑;
  • 4、就可以本地启动跑一下,验证拿到的最终处理数据是否正确 不正确,去调整对应的测试; 正确,就将返回结果当作最初的集成测试的验证数据,并让它通过;

这个时候就做完了。🤓

You need to Sign in before reply, if you don't have an account, please Sign up first.