美文网首页
测试遗留代码小技巧

测试遗留代码小技巧

作者: hxfirefox | 来源:发表于2018-06-10 23:00 被阅读7次
    coding

    修改代码是件很有趣的事,至少能成为我们养家糊口的工作,不过这份工作可不轻松,特别是遇上遗留代码,它会让你血压飚升、坐立不安甚至骂街暴走。

    遭遇难搞的遗留代码时的心情

    那么为何遗留代码会这么难对付呢?

    在业界,遗留代码这个词通常代码的是我们根本就不理解的难以修改的代码。但是经过和团队一起工作多年,帮助他们解决严重的代码问题,我得到了不同的定义。
    对我来说,遗留代码是缺少测试的简单代码。——《修改代码的艺术》

    缺少测试是遗留代码的一个显著缺陷,所以对症的方法就应该是给遗留代码添加测试,可就在添加测试时,遗留代码的险恶面目才暴露出来——各种花式不可测、 不好测的代码。

    遗留代码的困境
    当我们修改代码的时候,需要先准备好测试。想要准备好测试,我们往往不得不修改代码。——《修改代码的艺术》

    下面有个遗留代码的小例子,透过它可以一窥遗留代码的困境。

    public class Game {
        public static void main(String[] args) {
            Ship cv = new Ship(17, ShipType.CV)
            int x = Math.random() * 10;
            int y = Math.random() * 20;
            System.out.println(String.format("Ship %s-%d place on x:%d, y:%d", 
                cv.getShipType(), cv.getPennantNo(), x, y));
            cv.place(x, y)
    
            System.out.println("Please input postion wanted to be attacked")
            String attackPos = new BufferedReader(new InputStreamReader(System.in)).readLine();
            String[] pos = attackPos.split(" ")
            int attackX = Utils.trans(pos[0]);
            int attackY = Utils.trans(pos[1]);
            boolean result = attack(attackX, attackY, x, y);
            if (result) {
                System.out.println(String.format("hit on x:%d y:%d",attackX, attackY));
            } else {
                System.out.println(String.format("miss on x:%d y:%d",attackX, attackY));
            }
        }
    }
    

    分析上述代码不难发现阻碍测试的几个问题。

    • 代码逻辑均位于main方法中
    • 使用了随机值方法,测试中无法获取确定值
    • 输入输出均依赖控制台

    消除障碍的关键就在于设法解除这些问题代码中对于某些特定对象、类或者方法的依赖,这就要用到分离感知

    分离

    分离是指当我们无法把一段代码放到测试用具中执行的时候,我们就会打破依赖以分离。在上面的例子中的main方法和输出依赖就属于这类情况。很明显,这些代码是不能在测试框架中执行的,这就使得使用了这些代码的部分变得不可测。

    运用分离打破依赖就是要把无法在测试框架中运行的代码行为委托给可执行的对象来完成。

    • 针对main方法,我们通过简单的变换就可以做到功能代码被测试框架运行,如下所示通过变化将原来不可测的功能代码转化为可在测试框架中调用的play方法,但play方法仍然不太友好,因为它缺少外部特征能够被测试观察到,所以还需要进行一步地进行调整。
    public static void main(String[] args) {
        Game game = new Game();
        game.play();
    }
    
    public void play() {
        Ship cv = new Ship(17, ShipType.CV)
        int x = Math.random() * 10;
        int y = Math.random() * 20;
        ...
    }
    
    • 针对输出依赖,则需要将其提取并委托给指定的输出服务对象来进行处理,如下所示。
    public class Game {
        private final InOut io;
    
        public static void main(String[] args) {
            Game game = new Game(new InOut());
            game.play();
        }
    
        public Game(InOut io) {
            this.io = io;
        }
    
        public void play() throws IOException {
                ....
            boolean result = attack(attackX, attackY, x, y);
            if (result) {
                io.write(String.format("hit on x:%d y:%d", 
                    attackX, attackY));
            } else {
                io.wirte(String.format("miss on x:%d y:%d", 
                    attackX, attackY));
            }
        }
    }
    
    public class InOut {
        public void write(String format) {
            System.out.println(format);
        }
    }
    

    在经过这样的处理后,play方法出现了可以被观测的外部特征,并测试中实现如下:

    @RunWith(MockitoJunitRunner.class)
    public class GameTest {
        @Test
        public void should_out_hit_message_when_ship_is_hit() throws Exception {
            // given
            Game game = new Game(mock(InOut.class))
            // when
            ...
            game.play();
            // then
            verify(io).write("hit on x:5 y:13");
        }
    }
    

    感知

    感知是指当无法访问我们的代码所计算的值时,我们就会打破依赖以感知,在上面的例子中随机值产生以及输入依赖就属于这类情况。

    int x = Math.random() * 10;
    int y = Math.random() * 20;
    ...
    String attackPos = new BufferedReader(new InputStreamReader(System.in)).readLine();
    

    测试框架无法访问这段代码计算的准确值,也就造成不好测、不可测的问题。

    解决的方法在于打破依赖,具体来讲就是封装这些无法感知的代码,将他们变成为可以通过外部注入的对象。

    • 针对随机值,可以封装一个产生随机值的服务对象,并将其注入到原有代码,如下所示。
    public class RandomService {
        public double random() {
            return Math.random();
        }
    }
    
    public class Game {
    
        private final InOut io;
        private final RandomService randomService;
    
        public static void main(String[] args) {
            Game game = new Game(new InOut(), new randomService());
            game.play();
        }
    
        public Game(InOut io, RandomService service) {
            this.io = io;
            this.randomService = service;
        }
    
        public void play() throws IOException {
            Ship cv = new Ship(17, ShipType.CV)
            int x = randomService.random() * 10;
            int y = randomService.random() * 20;
            ...
        }
    }
    
    • 针对控制台输入的调整也类似,如下所示。
    public class InOut {
        public String read() throws IOException {
            return new BufferedReader(new InputStreamReader(System.in)).readLine();
        }
    
        public void write(String format) {
            System.out.println(format);
        }
    }
    

    在利用感知完成修订后,现在原来的遗留代码就成为了可测试的代码,我们可以在测试过程中通过替换注入的服务对象来产生不同的输入,从而验证我们期望的结果,如下所示。

    @RunWith(MockitoJunitRunner.class)
    public class GameTest {
        @Mock
        private InOut io;
        @Mock
        private RandomService randomService;
    
        private Game game;
    
        @Before
        public void setUp() {
            game = new Game(io, randomService);
        }
    
        @Test
        public void should_out_hit_message_when_ship_is_hit() throws Exception {
            // given
    
            // when
            when(io.read()).thenReturn("5 14");
            when(radomServcie.random()).thenReturn(0.5).thenReturn(0.7);
            game.play();
            // then
            verify(io).write("hit on x:5 y:14");
        }
    }
    

    改这么多代码不怕吗?

    这个真的不用担心,如何你读过之前的文章《快捷键,了解一下》那你就能明白,上面的这些改动对你来说可能只是写4~5行简单代码,其余复杂的操作都由IDE帮你搞定,所以放心大胆地利用IDE提供的便捷去搞定那些难搞的遗留代码吧!

    相关文章

      网友评论

          本文标题:测试遗留代码小技巧

          本文链接:https://www.haomeiwen.com/subject/pudxeftx.html