跳至主要內容

编码与调试

xmut-lby大约 14 分钟

编码与调试

你应该测试你的代码

所有代码都需要测试,没有测试的代码毫无价值

js里如何测试你的代码——直接在console里调用:

solution_02(1,2,3,4)
10

直接跑一下就可以。 以上面的测试为例,我们输入是参数(1,2,3,4),你预计的输出10。 一组输入和输出称为一个用例

如何你得到的输出结果和用例的预期输出不同,就说明你代码有问题了。前提当然你用例正确。 测试用例是我们测试代码的重要方式,你们的OJ和我自动判分的方式,都使用这种方式来执行。

测试用例之所以如此重要,在于一点:跑测试用例会让代码跑起来。 注意我们的原则:任何代码,不跑就什么都不是 跑用例是最贴近代码实际运行的测试方式,因此他不可替代。 那么要怎么写测试用例,才能“又快又好”呢?

设计你的用例

基本原则

我们跑用例的目的是保证代码能真正跑起来以验证逻辑的正确性。编写用例的原则也是一样:尽可能跑遍你的代码

这个原则是很容易做到的,因为你知道自己代码写成啥样,以上次的练习4为例:

let onClickGetFactors = function(){
    let tag = document.getElementById("num");
    let num = parseInt(tag.value);
    let output = document.getElementById("output");
    if((num<1) || (num>81)){
        output.innerHTML = "输入错误,n的范围必须在[1,81]之间";
        return;
    }
    let ret = solution_03(num);
    if( ret == null ){
        output.innerHTML = "无法拆分";
    }
    else{
        output.innerHTML = num.toString()+"="+ret[0].toString()+"x"+ret[1].toString();
    }
}

这是我的解法。很明显,这个函数要完成3个功能:

  1. 当num超出范围时,输出提示信息,告知范围不对
  2. 根据solution_03的返回值,返回null时,告知无法拆分。
  3. solution_03返回有效数值时,写入解法。

这意味着,我们至少应该测对应的三种情况,因此我们这样构造:

  1. 90
  2. 11
  3. 12

这样就保证三种情况都测到,才能保证你所有代码都能跑到,我们把这称为“测试覆盖率”。最好的测试覆盖率当然是100%,但有些时候,这做不到,那就应该尽量提高。

更全面覆盖

平时经常做测试的童鞋可能已经注意到了,第一种状况里有两个条件:(n<1)||(n>81)),而我们的用例90,只针对(n>81)这种情况。(n<1)并没有测试到。 因此针对第一种情况,我们还应该加上(n<1)的情况,比如测0、-1,这样测试才全面。

因为你测了90,并不能保证你这个组合逻辑的前一个逻辑是正确的,例如有人干脆就忘了写,那么你用90测试,就没法把这种错误找出来。

这是第二条原则:所有条件的组合,都要测试到。 这样我们针对三种情况,就分别有了4个用例:

  1. 90、-1
  2. 11
  3. 12

我们进一步考虑,假如用户不小心将(n>81)写成(n>=81)。那么这种错误用上面的4个用例也是找不出来的。

边界条件有比较大的概率出问题,应该为边界增加测试用例

本例中,我们有两个边界:1和81,那么我们可以这样测试:针对边界1,我们测试0、1、2这三个数,针对81,我们测80,81、82这三个数字。也就是分别是边界外,边界内和边界上。这样我们用例的纠错程度进一步提高了。

总结一下

对于初学者,请记住这三个原则:

  1. 测试要跑遍所有代码,否则测试就是不完整的。
  2. 组合条件要单独测,保证每个条件都覆盖到。
  3. 边界条件要增加用例保证边界内外都测试到。

经过这些测试,我们不能说代码一定不存在问题,但我们可以说,我们大幅度降低了代码出问题的概率。

当然不管测试有多少条原则,如果你不做就什么都没得说了,不要想当然的认为代码就不会有问题。没有人可以保证写代码一定不出bug。我们往大了说,测试自己编写的代码是身为程序员一项重要的职业道德

要怎么调你的代码?

写代码不能保证不出问题,出了问题,就要调试,那么应该如何调试你的代码呢?

原则一:代码量越少,定位和调试越容易

代码越少,越容易定位问题,等你写了一堆代码之后再发现问题,你面对几百行代码可能会无从下手。因此最好的做法是,写一点,测一下再写一点再测一下。 以上面的例子,你写的时候,可以这样写:

let onClickGetFactors = function(){
    let tag = document.getElementById("num");
    let num = parseInt(tag.value);
    console.log(num);
}

我们要做的第一步,是从输入框里获取数字,以方便后面代入函数计算。如果你没把握,特别是对新手,功能划分越小你越省力。于是我们写下这三行代码,第三行代码console.log(num)并不是真正有用的代码,但我们必须有输出来判断是否正确。不要小看这三行代码,你有一千种可能把这三行代码写错。比如很多人在第一行上会用错函数,又或者很多人会把词拼错。

写完之后,就要测试和调试,测试一个代码你需要在让他跑起来,调代码你也需要触发代码执行。我们这个函数可以由页面上的按钮直接触发。但对于那些不能由页面UI触发的函数来说,JavaScript也有很方便的方法:直接用浏览器的console来跑。

F12打开调试界面,直接在console里输入函数名调用就行了:

onClickGetFators()

注意你要正确的调用函数,必须带括号和参数(如果有的话)。

这样代码就可以直接跑起来了。

我要再强调一次:代码不跑就没有有任何意义

原则二:先分析再定位、先定位再修改,不要猜

新手常常代码运行一出现错误,就随便找个地方改改,期望随手改一个代码就搞定。这是很不好的习惯:

  1. 瞎猫碰到死耗子的可能有多大?大概率不可能改对。
  2. 有可能你这修改只不过把你测试的用例搞对了,其他用例一样错。
  3. 即便行为对了,你也不知道为什么错,你收获不到经验

遇到错误了,请你:努力凭自己的能力找到问题并修改>>让别人帮你看分析问题在哪里并修改>>让别人告诉你要怎么改>>随便改

这几个方法对你的帮助,依次递减。 所以作为新手,请你:确保你知道自己每个错误的原因

既然修改错误不能靠瞎蒙,定位问题也一样不能靠瞎蒙。 很多时候,你需要一些辅助手段来帮助你定位错误。请看后面的例子。

原则三:修改之后,该测试的还要测试

改完之后很重要的一步是,当然是测试一下能不能跑,但这个测试不要马虎,即便简单测试能过,也要尽量将之前的测试都过一下。这称为“回归测试”,这是为了避免:

  1. 你以为改对,其实改错。
  2. 你可能改对,但引入新错。

回归测试在测试中占有很重要的位置,请不要遗漏。

例子

我们看一个例子:

<input type="number" id="input"><button onclick="onClick01()">提交</button>
<p id="output"></p>
let onClick01 = function(){
    let num = document.getElementByTagName("input").vaule;
    document.getElementById("output").innerHTML = num.toString();
}

你跑程序,一点反应都没有。现在先别瞎猜:

第一步:收集现场信息

打开F12看控制台console的输出:

请先把英语练熟。

Uncaught TypeError说明这是一个未被捕获和处理的类型错误。后面是具体的错误信息,他告诉你document.getElementByTagName不是一个函数。

最后的index.js:2告诉你这个错误在index.js的第二行,你点击可以在调试器里查看到代码的位置。

最下方的两行是调用栈,你可以从里面看出函数是如何调用的。越下面,调用层次越高,也就是越远离现场。越上方,调用层次越低,也就是越接近现场。也就是说,这个调用关系是这样: index.html第12行HTMLButtonElement.onclick函数被首先调用。

在onclick函数里调用了onClick01,index.js第2行出现了问题。

这些都是非常有用的信息,跟警察破案,医生诊断一样,都要先收集足够的信息才能做出判断。

第二步:定位问题原因

注意,推测和定位原因是所有步骤中最具创造力和艺术性的步骤,很多时候是需要灵光一闪的——这点仅限对老手而言。对于新手,没什么好说的,所有灵感都扎根于经验,先老老实实积累你的经验。

如果你第一次遇到这样的问题,而且代码经验不足,你可能无法一下就推测出问题的原因,你有下面的几个办法:

  1. 百度或者其他搜索引擎。请注意选择合适的内容,比如这个例子,你搜索第一行就可以得到不错的结果StackOverFlowopen in new window。这个页面的内容是最精确的。但你也有可能搜不到,此时你就要懂得策略,比如这里的document.getElementByTagName是一个比较具体的内容,你可以尝试去掉,只搜索Uncaught TypeError: is not a functionopen in new window
  2. 对照已有的,正确的代码。这里你不要太过于相信自己的眼睛,要尽量用工具来对:比如你可以将正确的代码和错误的代码放到同一个文本编辑器里,放在上下两行,一眼就能看出你写错函数了。
  3. 另一种比较常用的方式是,把正确的代码copy过来,在别人的代码上改,如果你运行后发现该错误消除了,那么一般就是你笔误了。你可以用ctrl+z返回一下,复制出来再用工具对比。
  4. 猜,没错猜着试一下。你把这行代码直接在console里输出打一下:注意手动输入,因为console窗口有函数提示功能,可以避免笔误。如果功能正常,还是一样,复制出来,用工具对比。

如果一切顺利,此时你应该已经知道问题的原因了,你写错了函数名,应该是getElemenetsByTagName。笔误是初学者非常常见的错误。

第三步:修改重新运行

修改之后,继续测试,还是不行,有些童鞋到这里就不行了,好一点的会觉得,我是不是改错了?不好一点的,会觉得你浏览器是不是针对我啊?或者,我电脑是不是坏掉了啊? 拜托:对菜鸟来说,只有你错,程序和电脑不会错!请相信你的电脑。

到这里,先不要急,我们还是按部就班来:

继续打开F12:

首先我们发现出现的错误变了,连行号也变了,说明我们前一个问题解决了。

我们做的修改并不是没有改进,至少向前进了一步。

看错误信息:“无法获取undefined的toString属性”。

这个做何解呢?

我们看代码:

document.getElementById("output").innerHTML = num.toString();

这行出的问题,说无法获得undefined的toString属性,意思就是num是undefined,所以你无法获得num的.toString函数来执行。 验证一下你的观点,很简单,只要在执行前我们打印一下num就可以了:

console.log(num);
document.getElementById("output").innerHTML = num.toString();

我们看,输出了undefined。

这就是我们前面说的必要的辅助手段。把你怀疑的问题打印出来,有助于你定位问题。

当然你也可以调试,断在这行之前,看num的取值:

右侧Watch里可以看到num:undefined。

注意代码执行的位置,也就是图中蓝色背景行的位置。如果你当前代码处在第2行,就不正确了。因为处于某一行时,表明下一步会运行该行,换言之,本行尚未执行。那么num=undefined就是合理的。但当代码运行到第三行,说明第二行执行的结果就不符合预期。

对于菜鸟,不要怕麻烦,多打印输出,多调试。你所有付出的东西都会得到回报

第四步:拆分逻辑

我们可以断定问题一定还在第一行,但什么原因导致我们还需要推测。

我们先观察这行代码:

let num = document.getElementsByTagName("input").vaule;

我们先调用document.getElementsByTagName("input")来获得input对象,然后用.value来获得其属性。这在逻辑上没有问题的。

但这段代码就是有问题,要如何定位?

我们可以看到这行代码运行了两个功能,必然是这两个问题中的一个,或者两个都有问题。这个逻辑有点复杂了,我们不好下手。这跟前面第一个原则是相通的:代码(逻辑)越复杂,错误越难定位

一行代码如果包含比较复杂的逻辑,会不利于调试和定位问题,初学者不要学。代码除了正确、好读之外,可维护性也是很重要的因素。对初学者,初次编码时,我会建议把可维护性放在正确性前面

你可能会觉得正确性排后面难以接受,事实上,你只要做好可维护性(可调试性),早晚代码都可以调到正确。反之则不然。

先让代码好调,调正确之后,再好好改成好读的

回到这个例子上,我们需要将该行拆开,触类旁通之下,下一行也应该拆分:

let onClick01 = function(){
    let tag_input = document.getElementsByTagName("input");
    console.log(tag_input);
    let num = tag_input.vaule;
    console.log(num);
    let tag_output = document.getElementById("output")
    console.log(tag_output)
    tag_output.innerHTML = num.toString();
}

顺便加上console.log来辅助打印诊断信息。

第五步:继续定位

继续跑:

有问题已在我们预料之中。不过我们获得了3个输出。把输出和代码对照起来分析

let onClick01 = function(){
    let tag_input = document.getElementsByTagName("input");
    console.log(tag_input); // 第一个输出
    let num = tag_input.vaule;
    console.log(num); // 第二个输出
    let tag_output = document.getElementById("output")
    console.log(tag_output) // 第三个输出
    tag_output.innerHTML = num.toString();
}

因此:

  • 第一个输出的是tag_input,他等于HTMLCollection[input#input, input:input#input]
  • 第二个输出的是num,他等于undefined(意料之中)
  • 第三个输出是tag_output,他等于<p id="output"></p>

发现什么问题没有?

tag_inputtag_output是同一种东西,都是某个页面元素,但他们的值却是不一样的!

所以,收集尽量多的数据、认真观察和分析才是定位错误的必由之路。此外,注意对比相同和类似代码的执行结果,会非常有助于定位问题。

从出问题的位置,以及输出来看,我们基本可以断定是tag_input的问题。那么是什么问题?又回到似曾相识的场景了:1百度 2对照 3copy 4尝试console调用。

我直接说答案,document.getElementsByTagName这个函数,返回的是一个数组(其实并不是,是一个HTMLCollection对象,但你可以把它当数组用)。因为他是个s,因为这几个带s的函数都会返回复数个对象,所以必须返回数组。只有ById函数,因为id是唯一的(这已经是第二次说了)。

事实上如果你不是第一次遇到相同的问题,当你看到HTMLCollection时就应该立刻意识到问题的原因了。任何精巧的定位步骤和手段,在你犯错后积累的经验面前都一钱不值

到此为止了

可能的话,请你把本次的所有内容手做一遍。 事实上到目前为止,依然还有问题没有找到。 你可以再尝试找一下。