15.3 使用安装和卸载

    unittest模块中,存在3个级别的安装和卸载。这里是3种不同的测试范围:方法、类和模块。

    • 在测试用例中使用setUp()和tearDown()方法:这些方法可以确保在TestCase类中,每个单独的测试方法都能够被正确地安装和卸载。我们通常会使用setUp()方法来创建单元中的对象以及所需要的模仿对象。通常我们不会在这些方法中做代价很大的事情(例如,创建整个数据库),因为这些方法会在每个测试方法的执行前后都被调用。
    • 在测试用例中使用setUpClass()和tearDownClass()方法:在同一个TestCase类中,这些方法只会执行一次安装和卸载。这些方法会为每个方法创建这样的调用顺序:setUp()、testMethod()、tearDown()。在这里进行数据库中测试数据或测试模型的创建和销毁是不错的选择。
    • 在模块中使用setUpModule()和tearDownModule()函数:这些函数会在模块内所有TestCase类创建之前执行一次性的安装。在运行TestCase类之前,可以在这里执行测试数据库的创建和销毁。
    • 很少情况下会需要定义所有的setUp()和tearDown()方法。有一些测试场景将会成为可测试性设计的一部分,这些场景的基本区别是集成的程度。如之前所提到的,在测试层次结构中会包含3层:隔离的单元测试、集成测试和系统测试。有以下几种方法,包含了测试工作中所用到的不同的安装和卸载功能。 - 没有集成——没有依赖:一些类或函数没有依赖。它们不会依赖于文件、设备、其他进程或其他主机。其他类中的一些外部资源可以被模仿。当TestCase.setUp()方法的开销和复杂度很小时,可以直接创建所需要的对象。如果要模仿的对象非常复杂,可以使用类级别的TestCase.SetUpClass()来降低模拟对象创建的开销,它将用在被测试的方法中。
    • 内部集成——有部分依赖:在类或模块之间进行自动化集成测试通常涉及复杂的安装情况。我们可能会有一个复杂的、类级别的 setUpClass()方法或模块级别的setUpModule()来预备集成测试的环境。当使用在第10章“用Shelve保存和获取对象”以及第11章“用SQLite保存和获取对象”所介绍的数据访问层时,就可以进行类定义和访问层的集成测试。其中包括测试数据库的初始化或 shelf 所需要的测试数据。
    • 外部集成:可以使用系统中更复杂的组件来进行自动化集成测试。在这些用例中,可能会需要依赖外部进程或创建数据库并使用数据来初始化。这种情况下,可以使用setUpModule()为模块中所有的TestCase类创建空数据库。当使用在第12章“传输和共享对象”中所介绍的RESTful Web服务或对第17章“模块和包的设计”中介绍的宏观编程(Programming In The Large,PITL)进行测试时,这种方式会很有用。

    有关单元测试的概念需要注意的是,被测试的单元并没有一种明确的定义。单元可以是一个类、一个模块、一个包,甚至是一个已经集成的软件组件的集合,仅仅需要从环境隔离,成为测试单元。

    在测试集成化测试时,需要明确知道被测试的组件,这点是重要的。我们不需要对Python的类库进行测试,它们有自己的测试。同样地,我们也不需要对OS进行测试。集成测试一定要重点测试我们所写的代码,而不是我们下载安装的代码。

    15.3.1 使用OS资源进行安装和卸载

    在许多情况下,测试用例会需要一个特殊的 OS 环境。当使用外部资源,例如文件、目录或进程时,可能会需要在测试之前对它们进行初始化,可能也需要在测试前将这些资源移除,也可能在测试最后卸载这些资源。

    假如有一个函数rounds _ final()用于对指定文件进行处理,需要测试在特殊情况下这个函数的行为,例如文件不存在,通常会看到以下结构的TestCases

    import os
    class TestMissing( unittest.TestCase ):
       def setUp( self ):
         try:
           os.remove( "p3_c15_sample.csv" )
         except OSError as e:
           pass
       def test_missingFile_should_returnDefault( self ):
         self.assertRaises( FileNotFoundError, rounds_final, "p3_c15

    sample.csv", )

    我们需要处理这样一种异常,试图移除一个不存在的文件。这个测试用例中的setUp()方法可以确保所需的文件不存在。一旦setUp()保证文件真的缺失,可以使用缺失的文件名 "p3c15_sample.csv" 作为参数来执行 rounds final()函数。我们期望它会抛出FileNotFoundError错误。

    注意,FileNotFoundError是Python中open()方法的默认行为。它根本不需要测试。这样就有一个重要的问题:为什么要测试一个内置的功能?如果我们进行的是黑盒测试,我们需要对外部接口的所有功能进行测试,包括所期望的默认行为。如果进行的是白盒测试,可能希望测试异常处理的 try:语句,它在 rounds _ final()函数内部。

    p3 c15 sample.csv文件名在测试中被重复使用了。有些人会认为即使是测试代码,也要应用DRY原则。关于这点有一个限制,在写测试时,执行多少这样的优化是有价值的。以下是建议。

    测试代码不够稳定是可以接受的。对应用作的很小的改动都会导致测试失败,这真的是好事情。测试只需要简单清楚,不需要稳定性和可靠性。

    15.3.2 结合数据库进行安装和卸载

    当使用数据库和ORM层时,会经常需要创建测试数据库、文件、目录或服务器进程。为了确保其他测试可以运行,在测试通过后可能会需要卸载测试数据库,而在测试失败后可能不会想要卸载数据库。不对数据库进行任何操作,就可以根据执行结果对失败的测试进行分析。

    在一个复杂的、多层的架构中,管理测试范围是重要的。回顾一下第11章“用SQLite保存和获取对象”中,我们不需要对SQLAlchemy的ORM层或SQLite数据库进行测试。在应用测试的外部,这些组件有它们自己的测试过程。然而,由于ORM层创建了数据库定义SQL 语句,并基于代码创建Python对象,因而不能够轻易对SQLAlchemy进行模仿并希望使用它的方式是正确的。我们需要对应用 ORM 层的使用方式进行测试,而无需对ORM层自身进行测试。

    在测试用例安装的情况中,有一种情况很复杂,会涉及创建数据库,并基于指定测试的示例数据来初始化数据库。在使用SQL时,会运行一个相当复杂的SQL DDL脚本来创建需要的表,然后使用另一个SQL DML来初始化这些表。至于卸载,会使用另外一个复杂的SQL DDL脚本来完成。

    这样的测试用例将会很冗长,因此我们将它分为 3 部分:一个用于创建数据库模型的函数、setUpClass()方法以及单元测试的其他部分。

    以下是创建数据库的函数。

    from p2_c11 import Base, Blog, Post, Tag, assoc_post_tag
    import datetime

    import sqlalchemy.exc
    from sqlalchemy import create_engine

    def build_test_db( name='sqlite:///./p3_c15_blog.db' ):
       engine = create_engine(name, echo=True)
       Base.metadata.drop_all(engine)
       Base.metadata.create_all(engine)
       return engine

    以上代码通过移除与ORM类相关的表,并重新创建新表来刷新数据库。这样是为了得到一个刷新的数据库,并且直到最后一次运行单元测试,这个数据库与改变后的设计是同步的。

    在这个例子中,我们使用了一个文件来创建SQLite数据库。我们可以使用SQLite数据库的in-memory功能来使得测试运行得更快。使用内存数据库的缺点是,无法使用存储的数据库文件来调试失败的测试。

    以下是使用TestCase子类的例子。

    from sqlalchemy.orm import sessionmaker
    class Test_Blog_Queries( unittest.TestCase ):
       @staticmethod
       def setUpClass():
         engine= build_test_db()
         
    Test_Blog_Queries.Session = sessionmaker(bind=engine)

         session= Test_Blog_Queries.Session()
         tag_rr= Tag( phrase="#RedRanger" )
         session.add( tag_rr )
         tag_w42= Tag( phrase="#Whitby42" )
         session.add( tag_w42 )
         tag_icw= Tag( phrase="#ICW" )
         session.add( tag_icw )
         tag_mis= Tag( phrase="#Mistakes" )
         session.add( tag_mis )

         blog1= Blog( title="Travel 2013" )
         session.add( blog1 )
         b1p1= Post( date=datetime.datetime(2013,11,14,17,25),
           title="Hard Aground",
           rst_text="""Some embarrassing revelation.
             Including and ⎕""",
           blog=blog1,
           tags=[tag_rr, tag_w42, tag_icw],
           )
         session.add(b1p1)
         b1p2= Post( date=datetime.datetime(2013,11,18,15,30),
           title="Anchor Follies",
           rst_text="""Some witty epigram. Including and ☀""",
           blog=blog1,
           tags=[tag_rr, tag_w42, tag_mis],
           )
         session.add(b1p2)
         blog2= Blog( title="Travel 2014" )
         session.add( blog2 )
         session.commit()

    由于我们定义了 setUpClass(),因此在这个类的测试运行前将会创建一个数据库。我们可以定义很多测试方法,它们共享一个公共的数据库配置。一旦完成了数据库的创建,就可以创建会话并插入数据。

    我们将把会话生成器对象作为类级别的属性,Test Blog Queries.Session = sessionmaker(bind=engine)。然后就可以将这个类级别的对象用在setUp()以及每个测试方法中。

    以下是setUp()和两个单独测试方法的定义。

    def setUp( self ):
       self.session= Test_Blog_Queries.Session()

    def test_query_eqTitle_should_return1Blog( self ):
       results= self.session.query( Blog ).filter(
         Blog.title == "Travel 2013" ).all()
       self.assertEqual( 1, len(results) )
       self.assertEqual( 2, len(
    results[0].entries) )

    def test_query_likeTitle_should_return2Blog( self ):
       results= self.session.query( Blog ).filter(
         Blog.title.like("Travel %") ).all()
       self.assertEqual( 2, len(results) )

    setUp()方法创建了一个新的、空的会话对象。这样会确保每次查询都必须生成SQL并从数据库获取数据。

    query eqTitle should _ return1Blog()测试会查找请求的Blog实例,并通过entries的关系导航到Post实例。请求的filter()部分不会测试应用的定义,它会调用SQLAlchemy和SQLite。最后断言中的results[0].entries会对类定义进行测试。

    query likeTitle should _ return2Blog()几乎完全是在对SQLAlchemy和SQLite进行测试。它并没有对我们的应用作任何有意义的测试,只是出现了titleBlog属性。这种测试主要放在初始化时期完成,这使得应用的API显得更明确,尽管它们没有提供像测试用例那样的价值。

    以下是另外两个测试方法。

    def test_query_eqW42_tag_should_return2Post( self ):
       results= self.session.query(Post)\
         .join(assoc_post_tag).join(Tag).filter(
           Tag.phrase == "#Whitby42" ).all()
       self.assertEqual( 2, len(results) )
    def test_query_eqICW_tag_should_return1Post( self ):
       results= self.session.query(Post)\
       .join(assoc_post_tag).join(Tag).filter(
         Tag.phrase == "#ICW" ).all()
       self.assertEqual( 1, len(results) )
       self.assertEqual( "Hard Aground", results[0].title )
       self.assertEqual( "Travel 2013",
    results[0].blog.title)
       self.assertEqual( set(["#RedRanger", "#Whitby42", "#ICW"]),
    set(t.phrase for t in results[0].tags))

    query eqW42 tag should return2Post()测试执行了一个复杂的查询,根据指定标签查找所有文章。它测试到了很多类定义中的关系。

    类似地,对于query eqICW tag should return1Post()测试也使用了一个复杂的查询。它测试了通过results[0].blog.title来完成从Post到所属Blog的过程,也测试到了通过set(t.phrase for t in results[0].tags)来完成从Post到相关的Tags集合的导航。我们必须使用一个显示的set(),因为SQL不保证结果的顺序。

    TestCase子类Test Blog Queries的重要之处在于它通过setUpClass()方法创建了一个数据库模型和一个特殊定义的行集合。对于数据库应用来说,这种安装测试是有帮助的。它可能会非常复杂,通常从文件或JSON文档中完成示例行的加载,而并不是直接编码在Python中。