Automated API Testing Laravel Using JWT and Codeception
It’s been a while since I wrote post about Laravel,
So in this section I’ll cover my research about automated testing in Laravel.
TL;DR
Wanna jump to complete source? Here we are
Automated API Testing
Automated test is an important key in agile practice. It is providing quick feedback from our code so we could determine that our code is working or not, especially when our code is complex enough to do manual tests.
We may testing our API using tools like postman or other tools. It is really fine to manually testing our API endpoints, but how to test a hundred or maybe thousand endpoints and keep it reliable? or imagine every endpoint has at least 2 or 3 scenarios and we have to cover all of scenarios every single time.
Yes, let your machine do test for you.
What we need?
- Laravel
- Codeception
- JWT Package
What are we creating?
- User login API
- User get profile API
- User create article API
Setup & Installation
- Install Laravel
- Install Codeception
- Install JWT Package
Just follow the instruction and we are ready to go. I’ll skip installation & configuration steps because it will take to long time, so I’ll focus in the main topic.
Testing User Authentication API
There are two scenarios that covered user authentication API. Let’s write our testing code under tests/api directory. Simply generate a cest file using codeception command:
./vendor/bin/codecept g:cest api UserAuthTest
- Existing user is success do authentication using valid credential data (scenario 1)
// tests
public function testLoginIsSuccess(ApiTester $I)
{
$I->wantToTest('login is success');
// create user data using factory
factory(\App\User::class)->create([
'email' => 'didik@gmail.com',
'password' => bcrypt('rahasia123')
]);
// send credential data
$I->sendPOST('api/auth', ['email' => 'didik@gmail.com', 'password' => 'rahasia123']);
// login success
$I->seeResponseCodeIs(200);
// check if returned user data is contain expected email
$I->seeResponseContainsJson(['email' => 'didik@gmail.com']);
}
- User is failed do authentication using invalid credential data (scenario 2)
public function testLoginIsFailed(ApiTester $I)
{
$I->wantToTest('login is failed');
// create user data
factory(\App\User::class)->create([
'email' => 'didik@gmail.com',
'password' => bcrypt('rahasia123')
]);
// send invalid credential data
$I->sendPOST('api/auth', ['email' => 'didik@gmail.com', 'password' => 'rahasia12311']);
// check expected response code
$I->seeResponseCodeIs(401);
}
Clearly testing steps right? Now, if we used TDD, we write tests first then we write actual code until our test is passes, then refactor until met our ideal code.
When we already wrote actual code then run this tests using command:
./vendor/bin/codeception run api UserAuthTestCest --steps
We will see passes tests
Here is our controller that make test passes
/**
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function auth(Request $request)
{
$credentials = $request->only(['email', 'password']);
try { // attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
} // all good so return the token
return response()->json(['email' => auth()->user()->email, 'token' => $token]);
} catch (JWTException $e) { // something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
}
Using JWTAuth, we attempt user credential an return a token if user is valid. Otherwise, we wrote code that has been defined in testing code.
Testing User Get Profile API
Next, we want to check if authenticated user is able to get user detail using valid token (scenario 3). Why we need token? Because we want to protect our route is only accessible with authenticated user.
/**
* @param ApiTester $I
*/
public function authenticatedUserSuccessFetchProfile(ApiTester $I)
{
$I->wantToTest('authenticated user success fetch profile');
// create user data
$user = factory(\App\User::class)->create([
'email' => 'didik@gmail.com',
'password' => bcrypt('rahasia123')
]);
// create valid token
$token = \Tymon\JWTAuth\Facades\JWTAuth::fromUser($user);
// set header token Authorization: Bearer {token}
$I->amBearerAuthenticated($token);
// send request
$I->sendGET('api/user');
// check expected response code
$I->seeResponseCodeIs(200);
// check if response data is same with our init user data
$I->seeResponseContainsJson(['email' => 'didik@gmail.com']);
}
We got authorization token using JWTAuth because in login section we used it for authentication method, so the generated token is valid to used inside tests.
Now run test:
./vendor/bin/codecept run api UserAuthTestCest:authenticatedUserSuccessFetchProfile --steps
Using jwt.auth middleware, it is easy to get user data using jwt token
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware(['jwt.auth']);
Testing Create Article API
There are many possible scenarios for this case but for simplicity we will use 4 scenarios
- Authenticated user success create article. (scenario 4)
- Authenticated user failed create article using empty title. (scenario 5)
- Unauthenticated user failed create article. (scenario 6)
- Unauthorized user failed create article using invalid token. (scenario 7)
Generate new API test using command:
./vendor/bin/codecept g:cest api Article/UserCreateArticle
A new test file will created under tests/api/Article directory
Wrap up to our test code:
/**
* @param ApiTester $I
*/
public function authenticatedUserSuccessCreateArticle(ApiTester $I)
{
$I->wantToTest('authenticated user success create article');
// init user
$user = factory(\App\User::class)->create([
'email' => 'didik@gmail.com',
'password' => bcrypt('rahasia123')
]);
// generate jwt token
$token = \Tymon\JWTAuth\Facades\JWTAuth::fromUser($user);
// set header authorization
$I->amBearerAuthenticated($token);
// send request article data
$I->sendPOST('api/user/articles', [
'title' => 'this is title',
'content' => 'This is content from user',
'is_published' => 'y'
]);
// check expected response code is 200 OK
$I->seeResponseCodeIs(200);
// see response json is containing our expected data
$I->seeResponseContainsJson(['title' => 'this is title', 'content' => 'This is content from user', 'is_published' => 'y', 'user_id' => $user->id]);
// see database row is containing our expected data
$I->seeRecord('articles', ['user_id' => $user->id, 'title' => 'this is title', 'content' => 'This is content from user', 'is_published' => 'y']);
}
/**
* @param ApiTester $I
*/
public function authenticatedUserFailedCreateArticleUsingEmptyTitle(ApiTester $I)
{
$I->wantToTest('authenticated user failed create article using empty title');
// init user data
$user = factory(\App\User::class)->create([
'email' => 'didik@gmail.com',
'password' => bcrypt('rahasia123')
]);
// generate authorization token
$token = \Tymon\JWTAuth\Facades\JWTAuth::fromUser($user);
// set request header
$I->amBearerAuthenticated($token);
$I->haveHttpHeader('Accept', 'application/json');
// send request article data
$I->sendPOST('api/user/articles', [
'title' => '',
'content' => 'This is content from user',
'is_published' => 'y'
]);
// check response code is 422 unprocessable entity
$I->seeResponseCodeIs(422);
}
/**
* @param ApiTester $I
*/
public function unauthenticatedUserFailedCreateArticle(ApiTester $I)
{
$I->wantToTest('unauthenticated user failed create article');
// set request header
$I->haveHttpHeader('Accept', 'application/json');
// send request article data
$I->sendPOST('api/user/articles', [
'title' => 'lorem ipsum',
'content' => 'This is content from user',
'is_published' => 'y'
]);
// see response code is 400 bad request since we didn't include authorization token
$I->seeResponseCodeIs(400);
}
/**
* @param ApiTester $I
*/
public function unauthorizedUserFailedCreateArticleUsingInvalidToken(ApiTester $I)
{
$I->wantToTest('unauthorized user failed create article using invalid token');
// set request header
$I->haveHttpHeader('Accept', 'application/json');
$I->amBearerAuthenticated('xxxxx');
// send request article header
$I->sendPOST('api/user/articles', [
'title' => 'lorem ipsum',
'content' => 'This is content from user',
'is_published' => 'y'
]);
// see response code is 400 bad request since we didn't include valid authorization token
$I->seeResponseCodeIs(400);
}
Article endpoint needs authenticated user to store data so we protect endpoint using jwt.auth. After we finished our actual code, then run test
./vendor/bin/codecept run api Article
Passes tests is our expected result
Run All API Tests
We may run through all API tests using command:
./vendor/bin/codecept run api
Conclusion
Yeah, writing tests is painful and need more time in early development phase. But if we have automated tests, it will more easier to maintain code, integrate code with other developer, and integrate with other automation tools.
Here our complete source:
Have a nice weekend and happy code, Folks!