月度归档:2016年10月

Bug定位方法

Bug定位入门

方法一:分解法

【适用场景】:最终结果依赖与其他用应用或者产品线的请求响应。

【方法】:先排查发出的请求是否正确;在排查响应端的响应结果是否正确;最后分解,缩小排查范围,帮助开发有效定位bug。

【举例】:

Bug描述:多次请求,每次返回的推荐关键字均不相同。

问题定位:

(1)通过查看源码,分词正常成功发送匹配请求

(2)通过firfox找到请求的响应

(3)通过向该suggest链接重发请求发现,suggest对相同的分词匹配到的数据,时有时无 因此判断,问题不是出现前端应用,而是出现在suggest不稳定。

方法二:代码走读debug法

【适用场景】:数据,环境正常,对应代码可见

【方法】:先走读整个代码,重点review与操作功能对应的处理代码;很多隐蔽性的bug通过第一步的review是查不出来的,这个时候就需要debug代码

【举例】:

Bug描述:批量处理进入市场的时候失败。

问题定位:

(1)Review对应的方法,业务逻辑正确,没有问题

(2)在代码中设置断点,使用问题数据,进行debug,发现 即map定义为了string,string型,而变量market却是Long型,因此获取market获取时,没有认识long型的market,做了空值处理。

方法三:排除法 【适用场景】:没有任何的头绪,errormassage不能提供任何有用的信息,拿不到对应的代码

【方法】:先排除掉自己想到的可能性,步步为营,层层分离,剩下不确认的,一般便是问题所在,即便是不能完全定位到问题,但是也可以有效的缩小需要进一步定位的范围。

【举例】:

Bug描述:在相同条件下,发布可成功,但编辑失败。

定位过程:

(1)数据库中校验当前数据正常

(2)查看用户信息正常

(3)同一个用户可发布成功

(4)排除上面的各个因素之后,可以推测到:在发布和编辑接口对用户信息的处理不一致

(5)而发布和编辑都调用的同一个c,应该是c的问题

如何重现难以重现的bug

生活中有这么一种现象:如果你关注某些东西,它就会经常出现在你眼前,例如一个不出名的歌手的名字,一种动物的卡通形象,某个非常专业的术语,等等等等。这种现象也叫做“孕妇效应”。还有类似的一种效应叫做“视网膜效应”,它讲的是:你有什么东西或者特质你就特别容易在别处发现你有的这类东西和特质。干了多年测试的我就会经常发现日常使用的系统中有很多的bug,而我老婆就发现不了。今天要说的事儿是“重现难以重现的bug”,这件事儿在本周共遇见了4次:第一次是微博上有一篇《程序员,你调试过的最难的 Bug 是?》(后面会附上);第二次是一个同事跟我抱怨,好几个bug难以重现特心烦,并问我怎么处理比较好;第三次是本周上线的产品出现了一个当时难以重现的bug,我们对它做了初步的分析;第四次是翻看史亮写的书《软件测试实战》,偶尔翻了翻,竟然看到一小节在写“处理难以处理的缺陷”。这时候,脑子里很多东西汇集到了一起,我想还是记录一下吧。下面是正文:

  也许测试人员(尤其是对新手来说)在工作过程中最不愿遇到的一件事情就是:在测试过程中发现了一个问题,觉得是bug,再试的时候又正常了。碰到这样的事情,职业素养和测试人员长期养成的死磕的习性会让她们觉得不能放过这个bug,但是重现这样的bug有时候需要花费大量的时间,有的时候还有一些盲目性(因为黑盒测试的缘故,很多内部状态是不可见的,因此无法获取有效的信息来做跟踪),效率较为低下。在实际工作中,时间和进度摆在那里,在经历了多次痛苦的失败尝试之后,测试人员的处理方法一般会有如下几种:1.向开发人员寻求帮助来重现bug;2.当做一个issue报给开发人员。可是这样的做法存在如下问题:

1.开发人员责任心不够强,不愿意花太多精力去求证这件事情,常见的回复就是:在我这儿没事儿啊,我也重现不了,bug关了吧。结果随后在生产系统上,bug又开始sui随机出现了。

2.就跟测试人员不擅长编码和调试一样,开发人员并不擅长找出bug。经过一番尝试以后,他们也找不出什么问题来,常见的回复同第一条是一样的。bug上线后又出现的宿命也是一样的。

这时候,真正的问题来了:如何捕捉难以重现的bug?这件事儿对于测试人员来说就这么难么?

答案并不那么乐观,重现“难以”重现的bug本来就是一件“难以”完成的事情。但“难以”并不是不可能,通过一系列的测试、分析方法,我们是能够抽丝剥茧把绝大部分隐藏的很深的bug揪出来的,当然有的时候你要考虑投入产出比,但投入产出比不是本篇要考虑的,本篇只讲一些我积累的经验。

为什么不能重现bug?

最大的原因就是:测试人员对被测物的了解还不够深入。

我曾经做过一段很长时间的收集和统计,那些被称作过“难以重现”的bug最后都可以分为如下几类:

1.环境的变更造成了bug难以重现,这里的环境包括了:基础软硬件环境(操作系统、网络、存储、中间件、容器等等),被测物自身发生了某些变更。环境的变更一般是由于多人共用环境造成的,也有少量情况下是系统内部或者时间触发的变更(这类bug非常难重现)。

2.没有找到真正引发bug的操作。这些操作往往是一些不怎么显而易见的操作,测试人员在不经意间完成,而又忽略了这一操作,以致难于重现bug。

3.没有找到真正会引发bug的操作序列。很多bug的重现需要满足多个条件。在满足多个条件的状态下,你做了对应的操作,bug才会被触发。

4.bug必须使用特殊的数据才会出现,测试人员没有意识到她使用的数据的特殊性。一种比较难搞的情况是:同一组输入,在不同情况下(不是不同的业务情况)输入会被转化成不同的数据。我曾经见到过这么个例子,程序员用系统当前时间作为随机数的种子来生成id,但是id设置的比较短,一个存储的操作使用这个id,在一些情况下,发生了冲突,当时做功能测试这种小概率事件耗费了测试人员大量时间也没有稳定重现,还是在性能测试的阶段测试了出来。

5.测试人员由于错误操作,出现了误报(这很常见)。比如,记得自己执行了step3,其实没有,或者没有正确执行step却觉得正确执行了。

怎样对付这样的bug呢?

我喜欢James Bach 说的那句话:测试就像CSI。CSI是Criminal Scene Investigation 的缩写,也是我非常喜欢的美国系列剧。

从我来看CSI的精髓在于:仔细观察,详细记录,科学分析,严密推理,有序求证,大胆假设,持续不懈,团队协作和一点儿运气。找bug其实和CSI探员做犯罪现场调查没什么太大区别。得知道,你工作的重要性一点儿不亚于CSI探员。

仔细观察:第一件事情就是要观察,观察你所做的一切操作和一切相关的系统反馈。在一开始,观察的重要性要远远大于思考,通过观察你获得蛛丝马迹,这些蛛丝马迹是你进行思考和假设的关键输入。例如,我在一次测试的过程中,发现做某种操作的时候会相当慢,极少数情况下还报错过一两次,当询问了开发人员后得知这个操作的后台实现步骤是:先查看数据是否在缓存中,如果不在,则从远端服务器请求数据。我抓住少数情况下会报错的这一现象,仔细观察它的出错信息后猜测报错并不是因为网络连接不稳定引起的,而是由于远端服务接口实现有问题引起的,后来重新设计了测试用例,果然找到了问题所在。如果不仔细观察出错信息,就会听信开发人员认为这是网络不稳定引发的正常issue而错过这个bug。

详细记录:详细记录你的操作步骤及返回结果非常有助于回朔问题,也有助于后续分析。准备一个word文档,和截图工具有时候非常必要。另外,在观察的时候,你不仅要注意被测物的最终返回,还需要观察过程中的一些中间状态,往往这些中间状态提供的信息才是解开问题的关键。这些中间状态一般会被记录在log文件中,因此知道你的被测物是如何记log的,log被记录在哪里非常重要。log给了你另外一个看系统的角度。log是有级别的,如果级别可以动态调整,在找比较难找的bug时,可以将log记录的级别调至最低(DEBUG级)让它们记录更多内容。利用系统的错误转储文件(比如linux的core dump文件,windows下也有相应的记录转储文件的方式)分析也是个不错的办法(需要较高技术能力),但一般建议测试人员把这些转储文件交给更专业的开发人员来分析。在我短暂的C++开发岁月中,有使用过GDB阅读转储文件的经历,那绝对不是愉快的回忆。你瞧,测试人员的主要工作是找出可重现的bug,并不是定位它们,不是么?

除了log,如果能有监控信息,也要查看他们。比如系统提供的监控平台,监控日志;应用自己的监控平台、监控日志(如果有的话);采用一些外部技术手段截取一些中间状态信息,如使用sniffer抓取通讯包,使用Fiddler截获HTTP报文内容;给运行程序插桩来查看内存,堆栈,线程,函数被调用情况等情况,如Jprofile,gpertool等等。

科学分析:对于黑盒测试人员来说,科学分析意味着你需要有一定的分析策略。我们需要采取一些形式化的方法来完成我们的分析。基于我的统计,缺陷难以重现有很大一部分原因是因为“没有找到真正引发bug的操作序列“。测试人员不可能像开发人员那样去跟入到代码内部,设置断点调试程序,他们主要的测试方式是直接来操纵被测物,并从外部观察被测物状态的改变。显而易见,“状态转换图分析法”是测试人员对付“难以重现bug”的最强有力武器之一。状态转化图能够帮助测试人员很好的选择操作路径,并且知道这么做有什么意义。“状态图转化法”绝对是测试人员值得花时间学习和研究的一种方法,你可以走的很深,也可以研究得很远(可以从MBT的方向进行拓展),限于篇幅,这里就不展开了。在这里推荐《探索吧!深入理解探索式软件测试》这本书,它的第八章对“状态转换”做了非常实用的描述。

上文分析的让bug难于重现的另一种原因是没有找到“真正引发bug的特殊数据”。我的常用做法是这样的:1.画出系统交互图(要真正弄清系统的边界,这很重要),并识别出每种交互会有什么样的输入、输出数据和中间数据,识别出这些数据的规约和格式,这样你就不会对数据有遗漏。2.考虑数据的等价类、边界值,对这些输入进行组合,分析数据之间是否有耦合关系,如果有耦合关系,弄明白关系是什么,设计违背这些关系的用例,最后采用组合测试工具初步生成测试集,再人工选取最可能出问题的数据集(直觉有时候非常管用)。

严密推理:天马行空对测试人员很重要,但是当你试图重现一个bug的时候,这并不是一个非常好的方法。抓住了蛛丝马迹,你就要推理是为什么产生了这种蛛丝马迹。限于工作性质,测试人员更多的会从:业务完整性、数据完整性、业务正确性、数据正确性等方面考虑问题。但是,如果测试人员对被测物的IT架构有比较深入了解的话,推理的范围会扩大到技术实现层面,如:多线程可能引发的问题,网络引发的问题,excepiton处理不当引发的问题,全局事务设计不当引发的问题,内存泄漏引发的问题,数据库表设计不合规引发的问题等等等等,这些会让你的分析推理能力如虎添翼。当然,如果限于条件,测试人员不具备这类能力,则应该在适当的时候请求开发人员协助。

有序求证:这里只有一点需要注意。那就是,在求证的过程中不要打散弹枪,按照你的推理一步一步的来,一个个推理的来验证,一次只引入一处修改。这样才能让你的捕虫网编制的足够细密。

大胆假设:有的时候,看似八竿子打不着的东西竟然存在着千丝万缕的联系,而你获取信息的过程总是一个由少及多的过程,一开始这些联系是无法被识别出来的。通过推理,逐步验证,你慢慢的识别出了大部分内在联系。但有时候这种逐步推进的工作也会有局限性,工作如果出现了瓶颈(你试遍了你所有的假设,都没有重现bug),这时候就需要发挥一点儿想象力了,天马行空这时候从一定程度上又变得有用起来,当然天马行空也不是无厘头,还得靠我们所谓的“灵光一闪”,这号称是潜意识在帮助你。CSI的剧情中不也总是出现这种柳暗花明的桥段么?

坚持不懈:话不多说,有的时候你差的就是那么一点儿点儿耐心。

团队协作:很多情况下,重现bug不是一个人能搞定的。我们需要测试环境ready,测试数据ready,各种监控、分析工具ready,各种不同的视角开拓思路、加深对被测试物的认识。这是team work!!!独行侠有时候很管用,但是终究有极限。这就是为什么CSI是一票人在做而不是一两个人在做。

一点儿运气:说实在的,有的时候重现bug就是靠运气,你不得不承认这一点。事实上很多美好的事情发生都得依靠运气,比如中彩票。要记住的一点是,运气是建立在你不懈努力的基础上的。如果你一张彩票不买,你肯定什么也中不了。但如果你坚持买上几年,中个五块十块甚至二百也不是梦。

Let it go:全试过了,连运气都没有。你只能放手,回到最上面我说的那两条了:找开发来帮忙,或者给开发报issue。btw,即使不能重现bug,也应该给开发人员提供更多信息:如log、dump文件、监控记录、屏幕截图等。做一个负责人的测试人员,把烦恼真实的留给下家,这很重要:)

Debugging

如何用python实现一个高自由度爬虫

本爬虫有以下几点:

  • 可以爬取一个上亿页面的站点,可以多开,暂停,继续
  • 可以自定义爬行路径,爬行延时
  • 能够自动发现新链接,加入队列

以下为代码:

爬虫配置文件,cnblogs_com.py,可以配置开始页面,爬行页面,保存页面,头信息,延迟,超时时间。

# coding=utf-8

start_urls = [
    'http://www.cnblogs.com/',
    'http://news.cnblogs.com/',
    'http://q.cnblogs.com/',
    'http://home.cnblogs.com/blog/all/',
]
find_urls = [
    r'^http://news\.cnblogs\.com/n/\d+/$',
    r'^http://q.cnblogs.com/q/\d+/$',
    r'^http://www\.cnblogs\.com/[a-zA-Z0-9\-_]+/p/\d+.html$',
    r'^http://www\.cnblogs\.com/[a-zA-Z0-9\-_]+/archive/\d+/\d+/\d+/\d+.html$',
    r'^http://www\.cnblogs\.com/[a-zA-Z0-9\-_]+/$',
    r'^http://www\.cnblogs\.com/[a-zA-Z0-9\-_]+/default\.html\?page=\d+$',
    r'^http://q\.cnblogs\.com/tag/',
]
save_urls = [
    r'^http://news\.cnblogs\.com/n/\d+/$',
    r'^http://q.cnblogs.com/q/\d+/$',
    r'^http://www\.cnblogs\.com/[a-zA-Z0-9\-_]+/p/\d+.html$',
    r'^http://www\.cnblogs\.com/[a-zA-Z0-9\-_]+/archive/\d+/\d+/\d+/\d+.html$',
]
headers = {
    "User-Agent": "Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
    "Referer": "http://www.hisearch.cn/",
}
delay = 2
timeout = 5

爬虫开始命令行,可以运行 python spider.py -s cnblogs_com start 开始爬取一个站点,也可多开,中间进程挂了下次可以继续爬

#!/usr/bin/python
# coding=utf-8


import argparse
from lib.Spider import Spider

allow_commands = ['start', 'clean']

if __name__ == '__main__':
    # 解析参数
    parser = argparse.ArgumentParser(description='General crawler')
    parser.add_argument('-s', '--site', help='site config file name', required=True)
    parser.add_argument('command', help='|'.join(allow_commands), type=str)
    args = parser.parse_args()
    command = args.command
    # 执行程序
    s = Spider(args.site)
    if command == 'start':
        s.start()
    elif command == 'clean':
        s.clean()
    elif command == 'restart':
        s.restart()
    else:
        print('%s is not in a valid command, allowed: %s' % (command, '|'.join(allow_commands)))

爬虫类: Spider.py 主要采用了leveldb来存储爬取的数据,leveldb能够对抓取的数据进行压缩,用redis做队列,redis的hyperloglog数据格式能够用非常少的内存来做url去重

# coding=utf-8
"""
爬虫类
"""

import time
import sys
import traceback
import logging
from logging.handlers import TimedRotatingFileHandler
import re
import redis
import uuid
import requests
from conf import settings
import leveldb
from lxml.html import fromstring


class Spider(object):
    site = None
    config = None
    que = None
    log = None
    db = None
    request = None

    def __init__(self, site):
        self.site = site
        self.load_config()
        self.que = redis.from_url(settings.REDIS_URI)
        self.log = self.get_logger()
        self.db = leveldb.LevelDB(settings.DATA_DIR + self.site, max_open_files=30)
        self.request = requests.session()

    def start(self):
        if not self.is_started():
            self.que.sadd('running_sites', self.site)
            for url in self.config.start_urls:
                self.que.pfadd(self.site + '_all', url)
                self.que.lpush(self.site + '_in', url)
        self.run()

    def run(self):
        while not self.que_is_empty():
            url = self.que.rpop(self.site + '_in').decode()
            html = self.get_page(url)
            if html is not None:
                data = self.get_data(html, url)
                if data:
                    self.store_data(url, data)
                self.find_more_links(html, url)
            time.sleep(self.config.delay)
        self.finish()

    def que_is_empty(self):
        if self.que.llen(self.site + '_in') == 0:
            return True
        else:
            return False

    def load_config(self):
        self.config = __import__('conf.sites.' + self.site, fromlist=['conf.sites.' + self.site])

    def is_started(self):
        if self.que.sismember('running_sites', self.site):
            self.log.info("%s is started yet." % (self.site))
            return True
        else:
            self.log.info("%s is not start." % (self.site))
            return False

    def get_page(self, url):
        html = None
        try:
            r = self.request.get(url, headers=self.config.headers, timeout=self.config.timeout)
            if r.ok:
                html = r.text
            r.close()
            self.log.debug("page_download: " + url)
        except:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            self.log.exception("download_error: " + url + ", " + str(exc_value),
                               exc_info=traceback.format_tb(exc_traceback))
        return html

    def get_data(self, html, url):
        for regxp in self.config.save_urls:
            if re.compile(regxp).match(url):
                return html
        return False

    def store_data(self, url, data):
        self.db.Put(url.encode(), data.encode())
        self.log.debug("page_saved: %s" % url)

    def find_more_links(self, html, url):
        try:
            page = fromstring(html, url)
            page.make_links_absolute(url)
            for element, attribute, link, pos in page.iterlinks():
                for regxp in self.config.find_urls:
                    if re.compile(regxp).match(link):
                        self.add_url(link)
                        break
        except:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            self.log.exception("find_more_links_error: " + url + ", " + str(exc_value),
                               exc_info=traceback.format_tb(exc_traceback))

    def add_url(self, url):
        if self.que.pfadd(self.site + '_all', url) == 1:
            key = url.encode()
            if key not in self.db.RangeIter(include_value=False, key_from=key, key_to=key):
                self.que.lpush(self.site + '_in', url)
                self.log.debug("page_found: " + url)
            else:
                self.log.debug("page_exist: " + url)

    def finish(self):
        self.que.srem('running_sites', self.site)
        self.que.delete(self.site + '_all')
        self.log.info('finished')

    def clean(self):
        self.que.srem('running_sites', self.site)
        self.que.delete(self.site + '_all')
        self.que.delete(self.site + '_in')
        self.log.info('cleaned')

    def restart(self):
        self.clean()
        self.start()

    def get_logger(self):
        logger = logging.getLogger('spider.' + self.site)
        hd = TimedRotatingFileHandler(settings.LOG_DIR + self.site + '.log', when='D', backupCount=30)
        formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
        hd.setFormatter(formatter)
        logger.addHandler(hd)
        logger.setLevel(logging.DEBUG)
        return logger

    def get_doc_id_by_url(self, url):
        return str(uuid.uuid5(uuid.NAMESPACE_URL, url))

python 清理html

def clean_html(html):
    """
    Copied from NLTK package.
    Remove HTML markup from the given string.

    :param html: the HTML string to be cleaned
    :type html: str
    :rtype: str
    """

    # First we remove inline JavaScript/CSS:
    cleaned = re.sub(r"(?is)<(script|style).*?>.*?(</\1>)", "", html.strip())
    # Then we remove html comments. This has to be done before removing regular
    # tags since comments can contain '>' characters.
    cleaned = re.sub(r"(?s)<!--(.*?)-->[\n]?", "", cleaned)
    # Next we can remove the remaining tags:
    cleaned = re.sub(r"(?s)<.*?>", " ", cleaned)
    # Finally, we deal with whitespace
    cleaned = re.sub(r" ", " ", cleaned)
    cleaned = re.sub(r"  ", " ", cleaned)
    cleaned = re.sub(r"  ", " ", cleaned)
    return cleaned.strip()

HTMLPrinter