chrismay's avatar

Api Test Structure (Model vs Controller vs Route Cest)

I want to test all my api endpoints with codeception.

I started off with a more model based approach:

class UserCest
{
    public function it_can_request_its_own_status (ApiTester $I) {
        $user = $I->amLoggedIn();
        $I->sendGET('/users/'.$user->id.'/status');
        $I->seeResponseCodeIs(200);
        $I->seeResponseContainsJson(['status' => 'active']);
    }
}

Now I realized that I might want to map the test directly to the routes to make it more obvious what route is tested. I thought that this would therefore rather refer to the controller that is used by the routes rather than models:

class UserControllerCest
{
    public users__user__status (ApiTester $I) {
        $route = __FUNCTION__;
        $user = $I->amLoggedIn();
        $url = $this->fillRouteWithUserId($route, $user);
        $I->sendGET($url);
        $I->seeResponseCodeIs(200);
        $I->seeResponseContainsJson(['status' => 'active']);
    }
}

Or I could even map the Cests directly to each route:

class users__user__statusCest
{
protected $route = '/users/{user}/status';

    public it_reports_the_status_for_authenticated_users (ApiTester $I) {
        $user = $I->amLoggedIn();
        $url = $this->fillRouteWithUserId($this->route, $user);
        $I->sendGET($url);
        $I->seeResponseCodeIs(200);
        $I->seeResponseContainsJson(['status' => 'active']);
    }

    public it_requires_authentication (ApiTester $I) {
        $user = $I->create(User::class)
        $url = $this->fillRouteWithUserId($this->route, $user);
        $I->sendGET($url);
        $I->seeResponseCodeIs(401);
    }
}

What (other) approach is suitable for testing my API?

0 likes
2 replies
chrismay's avatar

Ok, so the way I handle it now is to extend from an ApiCest class that is tightly coupled to a route.

UserStatusCest would now map to the route 'user.status'

It also tries to guess the REST method based on the name.

I would appreciate feedback on the design.

chrismay's avatar

abstract class ApiCest
{
    protected $route;
    protected $method;
    protected $authentication = true;
    protected $private = true;

    /** @var $user User */
    protected $user = null;

    public function __construct()
    {
        $this->route = $this->route ?? $this->getRouteFromClassName();
        $this->method = $this->method ?? $this->getMethodFromRouteName();
    }

    public function _before(ApiTester $I)
    {
    }

    public function _after(ApiTester $I)
    {
    }

    protected function send(ApiTester $I, $params) {
        $I->{'send'.$this->method}($this->url(), $params);
    }

    protected function url(string $route = null, User $user = null) : string {
        $route = $route ?? $this->route;
        $user = $user ?? $this->user;
        if($user)
            return route($route, ['user' => $user->id]);
        else
            return route($route);
    }

    private function getRouteFromClassName() : string {
        $className = str_replace('Cest', '', static::class);
        $className = lcfirst($className);
        $parts = preg_split('/(?=[A-Z])/', $className);
        $route = strtolower(implode('.', $parts));
        return $this->route = $route;
    }

    private function getMethodFromRouteName() : string {
        $parts = explode('.', $this->route);
        $keyword = strtolower(array_slice($parts, -1)[0]);
        $methodMap = [
            'GET' => [ 'get', 'index', 'read', 'find', 'all' ],
            'POST' => [ 'post', 'store', 'save', 'create' ],
            'PUT' => [ 'put', 'overwrite' ],
            'PATCH' => [ 'patch', 'update' ],
            'DELETE' => [ 'delete', 'destroy' ]
        ];
        $methodMapFlipped = [];
        foreach($methodMap as $method => $keywords) {
            foreach($keywords as $keyword) {
                $methodMapFlipped[$keyword] = $method;
            }
        }
        $this->method = $methodMapFlipped[$keyword];
        return $this->method;
    }

    public function it_implements_authentication_rules (ApiTester $I) {
        if($this->private) {
            $I->seePrivateRoute($this->route, $this->method);
        }
        else if ($this->authentication) {
            $I->seeAuthRoute($this->route, $this->method);
        }
    }
}

Please or to participate in this conversation.