mock mvc 测试_实用的OOP:构建测验应用-MVC

mock mvc 测试

In part one of this series we began, using the bottom-up design approach, by creating our Quiz and Question entities, writing a dummy Data Mapper class for the Quiz entities, and sketching the interface for our main service, \QuizApp\Service\Quiz, which will define the flow for a user solving a quiz. If you haven’t yet read the first part, I suggest you quickly skim through it before continuing with part two and/or download the code from here.

在本系列的第一部分中,我们使用自下而上的设计方法,创建了QuizQuestion实体,为Quiz实体编写了一个虚拟的Data Mapper类,并为主要服务\QuizApp\Service\Quiz绘制了接口\QuizApp\Service\Quiz ,它将为用户解决测验定义流程。 如果您尚未阅读第一部分,建议您先快速浏览一下,然后再继续第二部分和/或从此处下载代码。

This time we’ll create and flesh out the \QuizApp\Service\Quiz service that will be the backbone of our quiz app. We’ll then write our controllers and views using the Slim MVC framework, and, finally, create a MongoDB mapper to take the place of the dummy mapper we wrote last time.

这次,我们将创建并\QuizApp\Service\Quiz服务,该服务将成为我们测验应用程序的基础。 然后,我们将使用Slim MVC框架编写我们的控制器和视图,最后,创建一个MongoDB映射器来代替上次编写的虚拟映射器。

服务编码: (Coding the Service:)

Okay, now that we’ve defined the interface for the mapper and created the entity classes, we have all the building blocks we need for implementing a concrete service class.

好的,既然我们已经定义了映射器的接口并创建了实体类,那么我们就拥有了实现具体服务类所需的所有构造块。

<?php

namespace QuizApp\Service;

use QuizApp\Service\Quiz\Result;

// ...

class Quiz implements QuizInterface
{
    const CURRENT_QUIZ = 'quizService_currentQuiz';
    const CURRENT_QUESTION = 'quizService_currentQuestion';
    const CORRECT = 'quizService_correct';
    const INCORRECT = 'quizService_incorrect';

    private $mapper;

    public function __construct(\QuizApp\Mapper\QuizInterface $mapper)
    {
        $this->mapper = $mapper;
    }

    /** @return Quiz[] */
    public function showAllQuizes()
    {
        return $this->mapper->findAll();
    }

    public function startQuiz($quizOrId)
    {
        if (!($quizOrId instanceof \QuizApp\Entity\Quiz)) {
            $quizOrId = $this->mapper->find($quizOrId);
            if ($quizOrId === null) {
                throw new \InvalidArgumentException('Quiz not found');
            }
        }

        $_SESSION[self::CURRENT_QUIZ] = $quizOrId->getId();
        $_SESSION[self::CORRECT] = $_SESSION[self::INCORRECT] = 0;
    }

    /**
     * @return Question
     * @throws \LogicException
     */
    public function getQuestion()
    {
        $questions = $this->getCurrentQuiz()->getQuestions();
        $currentQuestion = $this->getCurrentQuestionId();
        if ($this->isOver()) {
            throw new \LogicException();
        }
        return $questions[$currentQuestion];
    }

    /** @return bool */
    public function checkSolution($solutionId)
    {
        $result = $this->getQuestion()->isCorrect($solutionId);
        $_SESSION[self::CURRENT_QUESTION] = $this->getCurrentQuestionId() + 1;
        $this->addResult($result);
        if ($this->isOver()) {
            $_SESSION[self::CURRENT_QUESTION] = $_SESSION[self::CURRENT_QUIZ] = null;
        }
        return $result;
    }

    /** @return bool */
    public function isOver()
    {
        try {
            return $this->getCurrentQuestionId() >= count($this->getCurrentQuiz()->getQuestions());
        } catch (\LogicException $e) {
            return true;
        }
    }

    /** @return Result */
    public function getResult()
    {
        return new Result(
            $_SESSION[self::CORRECT], $_SESSION[self::INCORRECT],
            ($_SESSION[self::CORRECT] + $_SESSION[self::INCORRECT]) / 2
        );
    }

    private function getCurrentQuiz()
    {
        if (!isset($_SESSION[self::CURRENT_QUIZ])) {
            throw new \LogicException();
        }
        $quiz = $this->mapper->find($_SESSION[self::CURRENT_QUIZ]);
        if ($quiz === null) {
            throw new \LogicException();
        }
        return $quiz;
    }

    private function getCurrentQuestionId()
    {
        return isset ($_SESSION[self::CURRENT_QUESTION]) ? $_SESSION[self::CURRENT_QUESTION] : 0;
    }

    private function addResult($isCorrect)
    {
        $type = ($isCorrect ? self::CORRECT : self::INCORRECT);
        if (!isset($_SESSION[$type])) {
            $_SESSION[$type] = 0;
        }
        $_SESSION[$type] += 1;
    }
}

That’s a long one. Let’s go over it method by method.

那是一个很长的时间。 让我们逐个方法进行研究。

The showAllQuizes() method wraps the QuizMapper::findAll() method. We could make $mapper public, but that would break encapsulation, leaking low-level operations to high-level classes.

showAllQuizes()方法包装QuizMapper::findAll()方法。 我们可以将$mapper公开,但这会破坏封装,将低级别的操作泄漏给高级别的类。

The startQuiz() method begins the quiz that is passed as an argument by storing the quiz in the session for future reference. It accepts either a quiz entity object or a quiz ID. In the latter case it tries to find the quiz using the $mapper. The method uses the $_SESSION superglobal directly, which isn’t best practice–the service would break if used in a command-line context, for instance–but there’s no need to over-complicate the service yet. If we wanted to make the service runnable on the command-line, we’d extract the operations we used for storing data in the session to an interface. The web controller would pass an implementation that internally used the $_SESSION superglobal, while the command-line controller might store the “session” variables in as class properties.

startQuiz()方法通过将测验存储在会话中以供将来参考来开始作为参数传递的测验。 它接受测验实体对象或测验ID。 在后一种情况下,它尝试使用$mapper查找测验。 该方法直接使用$_SESSION超级全局方法,这不是最佳实践-例如,如果在命令行上下文中使用该服务,则该服务将中断-但现在无需使该服务过于复杂。 如果要使该服务在命令行上可运行,则可以提取用于将会话中的数据存储到接口的操作。 Web控制器将通过内部使用$_SESSION超全局变量的实现,而命令行控制器可能将“会话”变量存储为类属性。

The getQuestion() method tries getting the next question of the current quiz from the database, delegating to other helpful methods, and throws an exception if the quiz is over or the user isn’t in the middle of a quiz. The checkSolution() method returns whether the user’s solution is correct, and updates the session to reflect the state of the quiz after the question is answered.

getQuestion()方法尝试从数据库中获取当前测验的下一个问题,委托其他有用的方法,如果测验结束或用户不在测验中间,则引发异常。 checkSolution()方法返回用户的解决方案是否正确,并在回答问题后更新会话以反映测验的状态。

The isOver() method returns true if the current quiz is over or if no quiz is underway.

如果当前测验结束或没有进行测验,则isOver()方法将返回true。

The getResult() method returns a \QuizApp\Service\Quiz\Result object that tells the user whether he passed the quiz and how many questions he answered correctly.

getResult()方法返回一个\QuizApp\Service\Quiz\Result对象,该对象告诉用户他是否通过了测验以及他正确回答了多少个问题。

Slim的控制器和视图: (Controllers and Views with Slim:)

Now that we’ve finished setting up the “M” of our MVC application, it’s time to write our controllers and views. We’re using the Slim framework, but it’s easy to replace Slim with any other MVC framework as our code is decoupled. Create an index.php file wih the following contents:

现在,我们已经完成了MVC应用程序的“ M”设置,是时候编写控制器和视图了。 我们正在使用Slim框架,但是随着我们的代码解耦,很容易用任何其他MVC框架替换Slim。 创建具有以下内容的index.php文件:

<?php

require 'vendor/autoload.php';

session_start();

$service = new \QuizApp\Service\Quiz(
    new \QuizApp\Mapper\HardCoded()
);
$app = new \Slim\Slim();
$app->config(['templates.path' => './views']);
// Controller actions here
$app->run();

This is the base of our Slim application. We create our service and start the PHP session, since we use the $_SESSION superglobal in our service. Finally, we set up our Slim application. For more information about Slim, read the extensive documentation on the Slim project website.

这是我们Slim应用程序的基础。 由于我们在服务中使用了$_SESSION超全局变量,因此我们创建了服务并启动了PHP会话。 最后,我们设置了Slim应用程序。 有关Slim的更多信息,请阅读Slim项目网站上的详细文档。

Let’s create the homepage first. The homepage will list the quizzes the user can take. The controller code for this is straightforward. Add the following by the comment in our index.php file.

让我们首先创建主页。 主页将列出用户可以参加的测验。 控制器代码很简单。 在index.php文件中的注释中添加以下内容。

$app->get('/', function () use ($service, $app) {
    $app->render('choose-quiz.phtml', [
        'quizes' => $service->showAllQuizes()
    ]);}
);

We define the homepage route with the $app->get() method. We pass the route as the first parameter and pass the code to run as the second parameter, in the form of an anonymous function. In the function we render the choose-quiz.phtml view file, passing to it the list of our quizzes we retrieved from the service. Let’s code the view.

我们使用$app->get()方法定义首页路由。 我们以匿名函数的形式将路由作为第一个参数传递,并将代码作为第二个参数传递以运行。 在该函数中,我们呈现choose-quiz.phtml视图文件,并将从服务中检索到的测验列表传递给该文件。 让我们对视图进行编码。

<h3>choose a quiz</h3>
    <ul>
        <?php foreach ($quizes as $quiz) : ?>
            <li><a href="choose-quiz/<?php echo $quiz->getId();?>"><?php  echo $quiz->getTitle(); ?></a></li>
        <?php endforeach; ?>
    </ul>

At this point, if you navigate to the home page of the app with your browser, you’ll see the two quizes we hard-coded earlier, “Quiz 1” and “Quiz 2.” The quiz links on the home page point to choose-quiz/:id where :id is the ID of the quiz. This route should start the quiz that the user chose and redirect him to his first question. Add the following route to index.php:

此时,如果您使用浏览器导航到应用程序的主页,您将看到我们之前硬编码的两个测验“测验1”和“测验2”。 主页上的测验链接指向choose-quiz/:id ,其中:id是测验的ID。 此路线应启动用户选择的测验,并将其重定向到他的第一个问题。 将以下路由添加到index.php

$app->get('/choose-quiz/:id', function($id) use ($service, $app) {
        $service->startQuiz($id);
        $app->redirect('/solve-question');
    });

Now let’s define the /solve-question route. This route will show the user the current question of the quiz he is solving.

现在让我们定义/solve-question路由。 此路线将向用户显示他正在解决的测验的当前问题。

$app->get('/solve-question', function () use ($service, $app) {
        $app->render('solve-question.phtml', [
            'question' => $service->getQuestion(),
        ]);
    }
);

The route renders the view solve-question.phtml with the question returned from the service. Let’s define the view.

该路由使用服务返回的问题呈现视图solve-question.phtml 。 让我们定义视图。

<h3><?php echo $question->getQuestion(); ?></h3>
<form action="check-answer" method="post">
    <ul>
        <?php foreach ($question->getSolutions() as $id => $solution): ?>
        <li><input type="radio" name="id" value="<?php echo $id; ?>"> <?php echo $solution; ?></li>
        <?php endforeach; ?>
    </ul>
    <input type="submit" value="submit">
</form>

We show the user a form with a radio button per answer. The form sends the results to the check-answer route.

我们向用户显示一个带有每个答案单选按钮的表单。 表格将结果发送到check-answer路线。

$app->post('/check-answer', function () use ($service, $app) {
        $isCorrect = $service->checkSolution($app->request->post('id'));
        if (!$service->isOver()) {
            $app->redirect('/solve-question');
        } else {
            $app->redirect('/end');
        }
    });

This time we’re defining a route for “POST” requests, so we use the $app->post() method. To get the solution ID sent by the user we call $app->request->post('id'). The service returns whether this answer was correct. If there are more questions to answer, we redirect him back to the “solve-question” route. If he’s finished the quiz, we send him to the “end” route. This should tell the user whether he passed the quiz and how many questions he answered correctly.

这次我们定义了“ POST”请求的路由,因此我们使用$app->post()方法。 要获取用户发送的解决方案ID,我们调用$app->request->post('id') 。 服务返回此答案是否正确。 如果还有其他问题要回答,我们会将其重定向回“解决问题”路线。 如果他完成了测验,我们会将他送至“终点”路线。 这应该告诉用户他是否通过了测验以及他正确回答了多少个问题。

$app->get('end', function () use ($service, $app) {
        $app->render('end.phtml', [
            'result' => $service->getResult(),
        ]);
    });

We do this by retrieving a \QuizApp\Service\Quiz\Result object from the service and passing it to the view.

我们通过从服务中检索\QuizApp\Service\Quiz\Result对象并将其传递给视图来实现。

<?php if ($result->hasPassed()) : ?>
    <h3>You passed!</h3>
<?php else: ?>
    <h3>You failed!</h3>
<?php endif; ?>
<p>You got <?php echo $result->getCorrect(); ?> out of <?php echo $result->getTotal(); ?> questions right.</p>
<a href="/">Back to quizes</a>

使用MongoDB编写真实的Mapper: (Writing a Real Mapper with MongoDB:)

At this point the app is complete, and will run properly–but we should write a real \QuizApp\Mapper\QuizInterface instance to connect to MongoDB. Right now we’re fetching our quizes from our Hardcoded mapper.

至此,该应用程序已经完成并且可以正常运行,但是我们应该编写一个真实的\QuizApp\Mapper\QuizInterface实例以连接到MongoDB。 现在,我们正在从Hardcoded映射器中获取测验。

Install MonogoDB if you don’t have it installed already.

如果尚未安装MonogoDB,请安装它。

We need to create a database, a collection, and propagate the collection with a dummy quiz. Run Mongo–mongo – and inside the terminal run the following commands:

我们需要创建一个数据库,一个集合,并通过一个虚拟测验传播该集合。 运行Mongo – mongo –并在终端内部运行以下命令:

> use practicaloop
    > db.quizes.insert((
          title: 'First Quiz',
          questions: [{
              question: 'Who\'s buried in Grant\'s tomb?',
              solutions: ['Jack', 'Joe', 'Grant', 'Jill'],
              correctIndex: 2
          }]
      })

Now we need to write another mapper that implements QuizInterface.

现在我们需要编写另一个实现QuizInterface的映射器。

<?php

namespace QuizApp\Mapper;

class Mongo implements QuizInterference
{
    private static $MAP = [];

    /** @var \MongoCollection */
    private $collection;

    public function __construct(\MongoCollection $collection)
    {
        $this->collection = $collection;
    }

    /**
     * @return \QuizApp\Entity\Quiz[]
     */
    public function findAll()
    {
        $entities = [];
        $results = $this->collection->find();
        foreach ($results as $result) {
            $entities[] = $e = $this->rowtoEntity($result);
            $this->cacheEntity($e);
        }
        return $entities;
    }

    /**
     * @param int $id
     * @return \QuizApp\Entity\Quiz
     */
    public function find($id)
    {
        $id = (string) $id;
        if (isset(self::$MAP[$id])) {
            return self::$MAP[$id];
        }
        $row = $this->collection->findOne(['_id' => new \MongoId($id)]);
        if ($row === null) {
            return null;
        }
        $entity = $this->rowtoEntity($row);
        $this->cacheEntity($entity);
        return $entity;
    }

    private function cacheEntity($entity)
    {
        self::$MAP[(string) $entity->getId()] = $entity;
    }

    private function rowToEntity($row)
    {
        $result = new \QuizApp\Entity\Quiz(
            $row['title'],
            array_map(function ($question) {
                return new \QuizApp\Entity\Question(
                    $question['question'],
                    $question['solutions'],
                    $question['correctIndex']
                );
            }, $row['questions'])
        );
        $result->setId($row['_id']);
        return $result;
    }
}

Let’s see what’s going on here. The class accepts a \MongoCollection as a constructor parameter. It then uses the collection to retrieve rows from the datbase in the find() and findAll() methods. Both methods follow the same steps: retrieve the row or rows from the database, convert the rows into our \QuizApp\Entity\Quiz and \QuizApp\Entity\Question objects, and caches them internally to avoid having to look up the same entities later.

让我们看看这里发生了什么。 该类接受\MongoCollection作为构造函数参数。 然后,它使用该集合在find()findAll()方法中从数据库检索行。 两种方法都遵循相同的步骤:从数据库中检索一行或多行,将这些行转换为我们的\QuizApp\Entity\Quiz\QuizApp\Entity\Question对象,并在内部对其进行缓存,以避免以后需要查找相同的实体。

All we have left to do is pass an instance of the new mapper to our service in the index.php file.

我们剩下要做的就是将新映射器的实例传递给index.php文件中的服务。

结论: (Conclusion:)

In this series, we built an MVC web application using the Service Layer and Domain Model design patterns. By doing so we followed MVC’s “fat model, thin controller” best practice, keeping our entire controller code to 40 lines. I showed you how to create an implementation-agnostic mapper for accessing the database, and we created a service for running the quiz regardless of the user-interface. I’ll leave it to you to create a command-line version of the application. You can find the full source for this part here.

在本系列中,我们使用服务层和域模型设计模式构建了MVC Web应用程序。 通过这样做,我们遵循了MVC的“胖模型,瘦控制器”的最佳实践,将整个控制器代码保持在40行之内。 我向您展示了如何创建用于访问数据库的与实现无关的映射器,并且我们创建了一个用于运行测验的服务,而不管用户界面如何。 我将它留给您来创建应用程序的命令行版本。 您可以在此处找到此部分的完整资源。

Comments? Questions? Leave them below!

注释? 有什么问题吗 把它们留在下面!

翻译自: https://www.sitepoint.com/practical-oop-building-quiz-app-mvc/

mock mvc 测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值