美文网首页
【Laravel5.2翻译】单元测试

【Laravel5.2翻译】单元测试

作者: Bill_Wang | 来源:发表于2016-09-02 10:53 被阅读1043次

    前阵子看了点Laravel源码,越看越乱,网上大部分中文文档都是直译,比较生涩难懂,还是决定看英文文档顺便就我的理解做下翻译整理记录下来

    思维导图

    简介

    Laravel构建的时候就带上测试。实际上,内含支持PHPUnit的测试,开箱即用,同时已经为你应用创建了phpunit.xml文件。框架给你提供了方便的帮助方法,可以让你形象地测试你应用。

    tests文件夹 提供了 ExampleText.php 文件。安装完Laravel应用,只要在命令行执行vendor/bin下的 phpunit就能运行你的测试

    测试环境

    当你运行测试,Laravel会自动为测试配置环境。测试的时候Laravel自动配置session和缓存到你的数组驱动,意味着测试的时候不会持久化session和缓存数据。
    需要的话,你可以自由的创建其他测试环境。测试环境的参数可以在phpunit.xml文件里配置,但要在运行测试前确保你用config:clear Artisan命令清理的配置缓存。

    定义&运行测试

    make:testArtisan命令创建一个新的测试案例:

    php artisan make:test UserTest
    

    这个命令会在tests文件夹下创建一个新的UserTest。然后你就可以像平时用PHPUnit一样定义你的测试方法。只要执行phpunit命令就可以运行测试:

    <?php
    
    use Illuminate\Foundation\Testing\WithoutMiddleware;
    use Illuminate\Foundation\Testing\DatabaseMigrations;
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    
    class UserTest extends TestCase
    {
        /**
         * A basic test example.
         *
         * @return void
         */
        public function testExample()
        {
            $this->assertTrue(true);
        }
    }
    

    注意:如果在测试类定义你自己的setUp方法,确保使用parent:setUp

    应用测试

    Laravel提供了非常流畅的API,可以让你向引用发送Http请求,检测输出,甚至填充表单。
    例如,看下ExamleTest.php

    <?php
    
    use Illuminate\Foundation\Testing\WithoutMiddleware;
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    
    class ExampleTest extends TestCase
    {
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->visit('/')
                 ->see('Laravel 5')
                 ->dontSee('Rails');
        }
    }
    

    visit方法向应用发送了一个Get请求。see方法确保在返回的消息中可以看到给定文本。dontSee方法确保返回消息中没有给定文本。这是Laravel可用的最基本的应用测试。

    和应用交互

    当然,比起确保文本出现在给定回复,你可以做更多。让我们来看些点击链接和填充表单的例子:

    点击链接
    在这个测试中,我们将对应用发起请求,在返回的响应中“点击”链接,然后确保登入指定URL。例如,我们假设在返回的响应 有一个文本为“About Us”的链接:

    <a href = "/about-us"> About Us</a>
    

    现在,让我们来写一个测试点击链接确保用户打开正确的页面:

    public function testBasicExample()
    {
        $this->visit('/')
             ->click('About Us')
             ->seePageIs('/about-us');
    }
    

    处理表单

    Laravel也为测试表单提供了多重方法。type,select,check,attack,和press方法允许你和所有的表单输入框做交互。例如,让我们想象一下应用的注册页面上有这样一个表单:

    <form action="/register" method="POST">
        {{ csrf_field() }}
    
        <div>
            Name: <input type="text" name="name">
        </div>
    
        <div>
            <input type="checkbox" value="yes" name="terms"> Accept Terms
        </div>
    
        <div>
            <input type="submit" value="Register">
        </div>
    </form>
    

    我们可以编写一个测试来填充表单来检查结果:

    public function testNewUserRegistration()
    {
        $this->visit('/register')
             ->type('Taylor', 'name')
             ->check('terms')
             ->press('Register')
             ->seePageIs('/dashboard');
    }
    

    当然如果表单包含其他输入比如单选按钮和下拉菜单,你也可以轻松的填充这些字段类型。这里列出每个表单的操作方法:

    方法 描述
    $this->type($text, $elementName) 输入文本
    $this->select($value, $elementName) 选择单选按钮和下拉框
    $this->check($elementName) 多选框选择
    $this->uncheck($elementName) 多选框取消选择
    $this->attach($pathToFile, $elementName) 添加附件
    $this->press($buttonTextOrElementName) 点击按钮

    处理附件

    如果表单包含file输入类型,你可以用attach方法关联

    public function testPhotoCanBeUploaded()
    {
        $this->visit('/upload')
             ->type('File Name', 'name')
             ->attach($absolutePathToFile, 'photo')
             ->press('Upload')
             ->see('Upload Successful!');
    }
    

    测试 JSON API

    Laravel也为测试JSON API和它们的响应提供了许多帮助。例如,get,post,put,patchdelete方法用来解决各种HTTP请求。你也可以轻松用这些方法传递数据和头文件。首先,让我们写一个测试,对/user发送一个POST请求然后确保给定的数组在返回的Json格式中:

    <?php
    
    class ExampleTest extends TestCase
    {
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->json('POST', '/user', ['name' => 'Sally'])
                 ->seeJson([
                     'created' => true,
                 ]);
        }
    }
    

    seeJson方法把给定数组转化成JSON,然后核实这个JSON片段是否在返回的JSON响应中出现。所以,就算返回的JSON里有其他的属性,只要给定片段存在依然可以通过测试。

    核实JSON精准匹配
    如果你像核实给定数组完全匹配应用返回的JSON,你可以用seeJsonEqual方法:

    <?php
    
    class ExampleTest extends TestCase
    {
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->json('POST', '/user', ['name' => 'Sally'])
                 ->seeJsonEquals([
                     'created' => true,
                 ]);
        }
    }
    

    验证JSON结构匹配
    你可以验证返回JSON是否特定结构。为此,你可以使用seeJsonStructure方法同时传递一系列嵌套关键字:

    <?php
    
    class ExampleTest extends TestCase
    {
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->get('/user/1')
                 ->seeJsonStructure([
                     'name',
                     'pet' => [
                         'name', 'age'
                     ]
                 ]);
        }
    }
    

    上面的例子期望收到一个name和一个包含nameage的嵌套对象pet。只要额外关键字带响应中存在seeJsonStructure就不会失败。比如,就算pet有一个weight属性测试也会通过。

    你可以使用*来确保返回的JSON结构里每一个列表项都至少包含设置的属性:

    <?php
    
    class ExampleTest extends TestCase
    {
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            // Assert that each user in the list has at least an id, name and email attribute.
            $this->get('/users')
                 ->seeJsonStructure([
                     '*' => [
                         'id', 'name', 'email'
                     ]
                 ]);
        }
    }
    

    你可以在嵌套中使用*,这样的话,你可以确保每个用户的数据里都包含给定的属性集同是每个pet属性都包含给定属性集:

    $this->get('/users')
         ->seeJsonStructure([
             '*' => [
                 'id', 'name', 'email', 'pets' => [
                     '*' => [
                         'name', 'age'
                     ]
                 ]
             ]
         ]);
    

    Session/认证

    Laravel为测试期间用session提供了一些帮助。首先,你可以用withSession方法把session的数据设置成指定数组。这个方法对请求前加载session很有用:

    <?php
    
    class ExampleTest extends TestCase
    {
        public function testApplication()
        {
            $this->withSession(['foo' => 'bar'])
                 ->visit('/');
        }
    }
    

    当然,seesion常见的应用就是保存用户状态,比如认证用户。actingAs方法提供了一个认证给定用户为当前用户的简便方法。比如,我们可以用model factory生成和认证一个用户:

    <?php
    
    class ExampleTest extends TestCase
    {
        public function testApplication()
        {
            $user = factory(App\User::class)->create();
    
            $this->actingAs($user)
                 ->withSession(['foo' => 'bar'])
                 ->visit('/')
                 ->see('Hello, '.$user->name);
        }
    }
    

    你也可以指定用哪个guard来认证给定用户,通过给actingAs方法的第二个参数传递一个guard名字:

    $this->actingAs($user, 'backend')
    

    中间件不可用

    在测试你的应用的时候,你会发现你可以很方便的在某些测试中让中间件不可用。这将使你可以在隔离中间件的情况下测试你的路由和控制器。Laravel包含一个WithoutMiddlware trait,你可以用他自动让所有的中间件不可用。

    <?php
    
    use Illuminate\Foundation\Testing\WithoutMiddleware;
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    
    class ExampleTest extends TestCase
    {
        use WithoutMiddleware;
    
        //
    }
    

    如果你只想对某些测试方法无效化中间件,你可以在测试方法中调用withoutMiddleware方法:

    <?php
    
    class ExampleTest extends TestCase
    {
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->withoutMiddleware();
    
            $this->visit('/')
                 ->see('Laravel 5');
        }
    }
    

    自定义HTTP请求

    如果你像发送自定义HTTP请求来获取完整的Illuminate\Http\Response对象,你可以使用call方法:

    public function testApplication()
    {
        $response = $this->call('GET', '/');
    
        $this->assertEquals(200, $response->status());
    }
    

    如果你创建一个POSTPUT或者PATCH请求,你可能需要传送一个输入数据的数组。当然,通过Request实例这些数据将在你的路由和控制器内可用:

    $response = $this->call('POST', '/user', ['name' => 'Taylor']);
    

    PHPUnit 断言

    Laravel 为PHPUnit提供了多个额外的断言方法:

    方法 描述
    ->assertResponseOk(); 确认客户端响应一个OK状态
    ->assertResponseStatus($code); 确认客户端响应一个指定代码
    ->assertViewHas($key, $value = null); 确认客户端响应视图里有给定绑定数据片段
    ->assertViewHasAll(array $bindings); 确认客户端响应视图有给定绑定数据队列
    ->assertViewMissing($key); 确认客户端响应视图缺失一对绑定数据
    ->assertRedirectedTo($uri, $with = []); 确认客户端是否重定向到指定URL
    ->assertRedirectedToRoute($name, $parameters = [], $with = []); 确认客户端是否重定向到指定路由
    ->assertRedirectedToAction($name, $parameters = [], $with = []); 确认客户端是否重定向到指定动作
    ->assertSessionHas($key, $value = null); 确认Session里有给定值
    ->assertSessionHasAll(array $bindings); 确认Session中有给定值集合
    ->assertSessionHasErrors($bindings = [], $format = null); 确认Session有错误绑定
    ->assertHasOldInput(); 确认Session中有旧的输入
    ->assertSessionMissing($key); 确认Session中缺失指定关键字

    数据库操作

    Laravel提供了各种各样有用的工具来简化测试我们的数据库驱动应用。首先,你可以用seeInDatabase来确认数据库里是存在符合你条件的数据。例如,如果你像验证在user表中有条数据它email值为sally@example.com,你可以这样写:

     public function testDatabase()
    {
        // Make call to application...
    
        $this->seeInDatabase('users', ['email' => 'sally@example.com']);
    }
    

    当然,seeInDatabase方法和其他类似的帮助方法都是为了方便。你可以自由使用任何PHPUnit的内建断言方法来支持你的测试。

    测试后重置数据库

    在测试后重置数据库是很有用的,这让后续测试不会受到前面测试的数据影响。

    使用Migration

    一个选择就是每次测试完都回滚数据库然后在下次测试前移植过去。Laravel提供了DatabaseMigrationstrait来自动进行这些操作:

    <?php
    
    use Illuminate\Foundation\Testing\WithoutMiddleware;
    use Illuminate\Foundation\Testing\DatabaseMigrations;
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    
    class ExampleTest extends TestCase
    {
        use DatabaseMigrations;
    
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->visit('/')
                 ->see('Laravel 5');
        }
    }
    

    使用会话
    另一个选择就是把每个测试案列包括在一个数据库会话中。同样的,Laravel提供了一个方便的DatabaseTransactionstrait来自动操作这些:

    <?php
    
    use Illuminate\Foundation\Testing\WithoutMiddleware;
    use Illuminate\Foundation\Testing\DatabaseMigrations;
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    
    class ExampleTest extends TestCase
    {
        use DatabaseTransactions;
    
        /**
         * A basic functional test example.
         *
         * @return void
         */
        public function testBasicExample()
        {
            $this->visit('/')
                 ->see('Laravel 5');
        }
    }
    

    注意:这个trait只是把默认的数据库连接包裹在会话中

    模块工厂

    在指定测试前,一般都需要插入一些记录到数据库中。Laravel允许使用"工厂"为你所有的Eloquent models定义一个属性集合,你就不用在你创建测试数据的时候手动指定每一列的值了。首先,让我们看一下database/factories/ModelFactory.php,开箱即用,这个文件包含一个工程定义:

    $factory->define(App\User::class, function (Faker\Generator $faker) {
        return [
            'name' => $faker->name,
            'email' => $faker->email,
            'password' => bcrypt(str_random(10)),
            'remember_token' => str_random(10),
        ];
    });
    

    在factory定义的闭包中,你可以返回模块上的所有属性的测试值。这个闭包会接受一个Faker PHP Library实例,它允许你很方便的生成各种随机的数据来测试。

    当然,你可以自由地在ModelFactory.php中添加自己的额外工厂。你也可以为每个model添加另外的工厂文件来更好的组织。比如,你可以在你的database/factories目录下创建UserFactory.phpCommentFactory.php文件。

    多样工厂类型

    有时候你像为同一个Eloquent model类创建多个工程。比如除了普通用户你还想为管理员用户添加工程。你可以用defineAs方法定义这些工程:

    $factory->defineAs(App\User::class, 'admin', function ($faker) {
        return [
            'name' => $faker->name,
            'email' => $faker->email,
            'password' => str_random(10),
            'remember_token' => str_random(10),
            'admin' => true,
        ];
    });
    

    如果不想从基础用户工厂中复制所有属性,你可以用raw方法来获取基类的所有属性。然后你只要任何你要加的值添加进去就可以了:

    $factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {
        $user = $factory->raw(App\User::class);
    
        return array_merge($user, ['admin' => true]);
    });
    

    在测试中用工厂
    当你定义好工厂,你可以用factory方法在你的测试或数据库seed文件中使用它们来生成model实例。让我们看一些创建model的例子。首先,我们用make方法,它会创建model但不会存入数据库:

    public function testDatabase()
    {
        $user = factory(App\User::class)->make();
    
        // Use model in tests...
    }
    

    如果你想重写一些你model中的默认值,你可以给make方法传递一个数组。只有指定的值会被替换,其他的值都会保留你在工厂中定义的默认值:

    $user = factory(App\User::class)->make([
        'name' => 'Abigail',
       ]);
    

    你也可以创建一个model集合或者创建一个给定类型的model:

    // Create three App\User instances...
    $users = factory(App\User::class, 3)->make();
    
    // Create an App\User "admin" instance...
    $user = factory(App\User::class, 'admin')->make();
    
    // Create three App\User "admin" instances...
    $users = factory(App\User::class, 'admin', 3)->make();
    

    持久化工厂model
    create方法不但创建model实例,还会用Eloquent的save方法把它们存入数据库:

    public function testDatabase()
    {
        $user = factory(App\User::class)->create();
    
        // Use model in tests...
    }
    

    同样的,你可以通过给create方法传数组来重写model属性:

    $user = factory(App\User::class)->create([
        'name' => 'Abigail',
       ]);
    

    为Model添加关联
    你或许想保存多个model到数据库。在这个例子中,你可以对一个已建model附加关联。当你用create方法创建多个models,会返回一个Eloquent collection实例,它让你可以用它提供的任何快捷函数,比如each

    $users = factory(App\User::class, 3)
               ->create()
               ->each(function ($u) {
                    $u->posts()->save(factory(App\Post::class)->make());
                });
    

    关联和属性闭包
    你可以在工厂定义中用属性闭包添加关联。比如,如果你想在创建Post的时候创建一个User,你可以这样做:

    $factory->define(App\Post::class, function ($faker) {
        return [
            'title' => $faker->title,
            'content' => $faker->paragraph,
            'user_id' => function () {
                return factory(App\User::class)->create()->id;
            }
        ];
    });
    

    这些闭包还能接收工厂的属性数组:

    $factory->define(App\Post::class, function ($faker) {
        return [
            'title' => $faker->title,
            'content' => $faker->paragraph,
            'user_id' => function () {
                return factory(App\User::class)->create()->id;
            },
            'user_type' => function (array $post) {
                return App\User::find($post['user_id'])->type;
            }
        ];
    });
    

    模仿

    模仿事件

    如果你在大量使用Laravel的事件系统,你可能希望在测试中有一个让事件安静下来或者模拟它。比如,如果你在测试用户注册,你可能不希望所有的UserRegistered事件操作被执行,因为它们会发送一个welcome电子邮件等等。
    Laravel提供了一个expectsEvents方法来验证预计的时间被执行,同时阻止任何这些时间的操作被执行:

    <?php
    
    class ExampleTest extends TestCase
    {
        public function testUserRegistration()
        {
            $this->expectsEvents(App\Events\UserRegistered::class);
    
            // Test user registration...
        }
    }
    

    你可以用doesntExpectEvents方法来验证给定事件没有被触发:

    <?php
    
    class ExampleTest extends TestCase
    {
        public function testPodcastPurchase()
        {
            $this->expectsEvents(App\Events\PodcastWasPurchased::class);
    
            $this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);
    
            // Test purchasing podcast...
        }
    }
    

    如果你像阻止所有的事件操作,你可以用withoutEvents方法:

    <?php
    
    class ExampleTest extends TestCase
    {
        public function testUserRegistration()
        {
            $this->withoutEvents();
    
            // Test user registration code...
        }
    }
    

    模拟工作

    有时候,当你发送请求给应用的时候你想测试特定工作是否被控制器分派下来。这允许你隔离工作逻辑来测试路由或者控制器。当然,你可以在独立的测试类里测试工作。

    Laravel提供了expectsJobs方法来验证预期的工作有没有分派下来,但不会执行工作:

    <?php
    
    class ExampleTest extends TestCase
    {
        public function testPurchasePodcast()
        {
            $this->expectsJobs(App\Jobs\PurchasePodcast::class);
    
            // Test purchase podcast code...
        }
    }
    

    注意:这个方法只会检索通过DispatchesJobs trait或者dispatch帮助函数分派下来的方法。它不会检索由Queue::push直接发送下来的工作。

    模拟门面

    在测试中,你可能经常希望模拟调用一个Laravel门面,例如,看一下下面的控制器动作:

    <?php
    
    namespace App\Http\Controllers;
    
    use Cache;
    
    class UserController extends Controller
    {
        /**
         * Show a list of all users of the application.
         *
         * @return Response
         */
        public function index()
        {
            $value = Cache::get('key');
    
            //
        }
    }
    

    你可以使用shouldReceive方法模拟调用Cache门面,它会返回一个Mockery实例。由于门面实际上由Laravel的服务容器处理和管理的,它们会比典型的静态类更容易测试。例如,让我们模拟条用Cache门面:

    <?php
    
    class FooTest extends TestCase
    {
        public function testGetIndex()
        {
            Cache::shouldReceive('get')
                        ->once()
                        ->with('key')
                        ->andReturn('value');
    
            $this->visit('/users')->see('value');
        }
    }
    

    注意:在你运行测试的时候,不要去模拟Request门面,你应该把你想要的输入传入HTTP帮助方法比如callpost

    相关文章

      网友评论

          本文标题:【Laravel5.2翻译】单元测试

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