【问题标题】:How to set up a test for a specific php function如何为特定的 php 函数设置测试
【发布时间】:2013-04-30 07:14:36
【问题描述】:

谁能帮我为这个 php 代码设置一个单元测试,我一直在尝试,但还没有找到解决方案。我已经从开发团队收到了这段代码,我应该为它创建单元测试关于如何去做的任何想法?

function checkbrute($user_id, $mysqli) {
   // Get timestamp of current time
   $now = time();
   // All login attempts are counted from the past 2 hours. 
   $valid_attempts = $now - (2 * 60 * 60); 

   if ($stmt = $mysqli->prepare("SELECT time FROM login_attempts WHERE user_id = ? AND time > '$valid_attempts'")) { `enter code here`
      $stmt->bind_param('i', $user_id); 
      // Execute the prepared query.
      $stmt->execute();
      $stmt->store_result();
      // If there has been more than 5 failed logins
      if($stmt->num_rows > 5) {
         return true;
      } else {
         return false;
      }
   }
}

【问题讨论】:

    标签: php unit-testing testing tdd phpunit


    【解决方案1】:

    如果测试涉及数据库(或网络、文件系统或任何其他外部服务),则它不是单元测试。除了任何定义之外,通过数据库进行测试很慢并且容易出错。模拟在动态语言中几乎太容易了,但许多人仍然通过数据库测试业务逻辑。我通常不喜欢发布问题的完整解决方案。在这种情况下,我觉得有必要这样做以证明它实际上很容易,尤其是在问题中描述的情况下。

    class CheckBruteTest extends PHPUnit_Framework_TestCase {
        public function test_checkbrute__some_user__calls_db_and_statement_with_correct_params() {
            $expected_user_id = 23;
            $statement_mock = $this->getMock('StatementIface', array());
            $statement_mock->expects($this->once())->method('bind_param')
                ->with($this->equalTo('i'), $this->equalTo($expected_user_id));
            $statement_mock->expects($this->once())->method('execute');
            $statement_mock->expects($this->once())->method('store_result');
    
            $db_mock = $this->getMock('DbIface', array());
            $time_ignoring_the_last_two_decimals = floor((time() - 2 * 60 * 60) / 100);
            $db_mock->expects($this->once())->method('prepare')
                ->with($this->stringStartsWith("SELECT time FROM login_attempts WHERE user_id = ? AND time > '$time_ignoring_the_last_two_decimals"))
                ->will($this->returnValue($statement_mock));
    
            checkbrute($expected_user_id, $db_mock);
        }
    
        public function test_checkbrute__more_then_five__return_true() {
            $statement_mock = $this->getMock('StatementIface', array());
            $statement_mock->num_rows = 6;
            $db_mock = $this->getMock('DbIface', array());
            $db_mock->expects($this->once())->method('prepare')
                ->will($this->returnValue($statement_mock));
    
            $result = checkbrute(1, $db_mock);
    
            $this->assertTrue($result);
        }
    
        public function test_checkbrute__less_or_equal_then_five__return_false() {
            $statement_mock = $this->getMock('StatementIface', array());
            $statement_mock->num_rows = 5;
            $db_mock = $this->getMock('DbIface', array());
            $db_mock->expects($this->once())->method('prepare')
                ->will($this->returnValue($statement_mock));
    
            $result = checkbrute(1, $db_mock);
    
            $this->assertFalse($result);
        }
    }
    interface DbIface {
        public function prepare($query);
    }
    abstract class StatementIface {
        public abstract function bind_param($i, $user_id);
        public abstract function execute();
        public abstract function store_result();
        public $num_rows;
    }
    

    由于我不知道函数的规范,我只能从代码中推导出来。

    在第一个测试用例中,我只检查数据库的模拟以及代码是否确实按预期调用这些服务的语句。最重要的是,它检查是否将正确的用户 ID 传递给语句以及是否使用了正确的时间限制。时间检查相当麻烦,但如果您直接在业务代码中调用 time() 之类的服务,就会得到这样的结果。

    第二个测试用例强制尝试登录次数为 6 并断言函数是否返回 true

    第三个测试用例强制尝试登录次数为 5 并断言函数是否返回 false

    这涵盖了(几乎)函数中的所有代码路径。只有一个代码路径被遗漏:如果$mysqli->prepare() 返回 null 或任何其他计算结果为false 的值,则绕过整个 if 块并隐式返回 null。我不知道这是不是故意的。代码应该明确表示这样的内容。

    出于嘲弄的原因,我创建了小接口和小抽象类。它们仅在测试的上下文中需要。也可以为$mysqli 参数和$mysqli->prepare() 的返回值实现自定义模拟类,但我更喜欢使用自动模拟。

    一些与解决方案无关的附加说明:

    • 单元测试是开发人员测试,应该由开发人员自己编写,而不是一些糟糕的测试人员。测试人员编写验收和回归测试。
    • 测试用例的“hackiness”说明了为什么在事后编写测试更加困难。如果开发人员编写了 TDD 风格的代码,代码和测试会更加简洁。
    • checkbrute 函数的设计并不理想:
      • 'checkbrute' 是个坏名字。它并没有真正讲述它的故事。
      • 它将业务代码与数据库访问混合在一起。时间约束的计算是业务代码以及对>5的检查。中间的代码是数据库代码,属于它自己的函数/类/任何东西。
      • 幻数。请使用常量作为幻数,例如 2h 值和最大登录尝试次数。

    【讨论】:

      【解决方案2】:

      单元测试很容易创建:它将数据放入数据库(带有插入),并检查函数对这些数据的返回是否良好。然后,当测试完成时,它会删除数据。

      在这里,您必须在 login_attempts 中插入值,例如:

      INSERT login_attempts(user_id, time, ...) VALUES (12, 1367849298)
      

      然后检查你的函数的返回值。

      【讨论】:

      • 我在哪里运行这段代码?你知道任何可以帮助我学习的好的教程或网站吗?
      • 您可以在任何地方运行此代码!检查此链接:phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html。我是通过在 Google 上搜索“php 单元测试”找到的……也许这会对你有所帮助。
      • 你知道如何测试没有类的函数吗?我得到了一个文件,其中包含很多我想进行单元测试的功能,但如果不使用类,我似乎无法让它工作。任何想法如何在不实现类的情况下解决这个问题?
      • 能否请您为我粘贴的代码编写一个工作单元测试,以便我了解它是如何工作的。
      • $deity 每次在同一个句子中使用“单元测试”和“数据库”时都会杀死一只小猫。
      猜你喜欢
      • 1970-01-01
      • 2023-03-12
      • 2014-02-14
      • 2022-06-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多