added more unit tests and configured test coverage discovery

This commit is contained in:
pushrbx 2023-02-13 21:15:07 +00:00
parent 75e04c72da
commit 331d5e821b
13 changed files with 186 additions and 44 deletions

View File

@ -2,6 +2,9 @@
namespace App\Exceptions; namespace App\Exceptions;
/**
* @codeCoverageIgnore
*/
class CustomTestException extends \Exception class CustomTestException extends \Exception
{ {
} }

View File

@ -2,6 +2,9 @@
namespace App\Support; namespace App\Support;
/**
* A class to provide strongly typed options about the cache configuration.
*/
final class CacheOptions final class CacheOptions
{ {
private ?int $ttl = null; private ?int $ttl = null;

View File

@ -50,11 +50,6 @@ final class CachedData implements ArrayAccess
return self::fromArray($model->toArray()); return self::fromArray($model->toArray());
} }
public function collect(): Collection
{
return $this->scraperResult;
}
public function isEmpty(): bool public function isEmpty(): bool
{ {
return $this->scraperResult->isEmpty(); return $this->scraperResult->isEmpty();
@ -85,11 +80,6 @@ final class CachedData implements ArrayAccess
return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl; return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl;
} }
public function cacheTtl(): int
{
return $this->cacheTimeToLive;
}
public function lastModified(): ?int public function lastModified(): ?int
{ {
if ($this->scraperResult->isEmpty()) { if ($this->scraperResult->isEmpty()) {

View File

@ -1,25 +0,0 @@
<?php
namespace App\Support;
/**
* @template T
*/
class Lazy
{
/**
* @param \Closure<T> $callback
*/
public function __construct(private readonly \Closure $callback)
{
}
/**
* @return T
*/
public function value()
{
$callback = $this->callback;
return $callback();
}
}

View File

@ -1,6 +1,9 @@
<?php <?php
namespace App\Testing\Concerns; namespace App\Testing\Concerns;
/**
* @codeCoverageIgnore
*/
trait MakesHttpRequestsEx trait MakesHttpRequestsEx
{ {
protected function getBaseUri(): string protected function getBaseUri(): string

View File

@ -4,6 +4,9 @@ use Faker\Generator;
use Faker\UniqueGenerator; use Faker\UniqueGenerator;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
/**
* @codeCoverageIgnore
*/
trait JikanDataGenerator trait JikanDataGenerator
{ {
/** /**

View File

@ -4,6 +4,9 @@ namespace App\Testing;
use Typesense\LaravelTypesense\Typesense; use Typesense\LaravelTypesense\Typesense;
/**
* @codeCoverageIgnore
*/
trait ScoutFlush trait ScoutFlush
{ {
protected array $searchIndexModelCleanupList = [ protected array $searchIndexModelCleanupList = [

View File

@ -8,6 +8,7 @@ use Illuminate\Support\Facades\DB;
/** /**
* A trait for test cases which want to clear the database after each test * A trait for test cases which want to clear the database after each test
* @codeCoverageIgnore
*/ */
trait SyntheticMongoDbTransaction trait SyntheticMongoDbTransaction
{ {

View File

@ -10,6 +10,7 @@ use TypeError;
/** /**
* An exception handler class which helps to highlight errors during tests. * An exception handler class which helps to highlight errors during tests.
* @codeCoverageIgnore
*/ */
class TestExceptionsHandler extends Handler class TestExceptionsHandler extends Handler
{ {

View File

@ -66,9 +66,7 @@ class IntegrationTestListener implements TestListener
try { try {
$kernel->call('migrate:fresh', []); $kernel->call('migrate:fresh', []);
} catch (\Exception $ex) { } catch (\Exception $ex) {
print_r($ex->getMessage()); dd($ex);
print_r($ex);
throw $ex;
} }
} }
} }

View File

@ -0,0 +1,81 @@
<?php
/** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\Unit;
use App\Support\CachedData;
use Illuminate\Support\Carbon;
use MongoDB\BSON\UTCDateTime;
use Tests\TestCase;
final class CachedDataTest extends TestCase
{
public function dateTimeProvider(): array
{
$now = Carbon::now();
return [
"mongodb bson date time" => [
$now, new UTCDateTime($now->getPreciseTimestamp(3))
],
"built-in datetime" => [
$now, $now->toDateTime()
],
"atom string datetime" => [
$now, $now->toAtomString()
]
];
}
public function invalidModifiedAtValuesProvider(): array
{
return [
"number value" => [
["modifiedAt" => 1]
],
"float value" => [
["modifiedAt" => 1.3]
],
"bool value" => [
["modifiedAt" => true]
],
"modifiedAt key not present" => [
["someotherkey" => 1]
]
];
}
public function testLastModifiedReturnsNullIfInternalCollectionIsEmpty()
{
$sut = CachedData::from(collect());
$this->assertNull($sut->lastModified());
}
/**
* @dataProvider invalidModifiedAtValuesProvider
*/
public function testLastModifiedReturnsNullIfModifiedAtIsUnknownFormat(array $contents)
{
$sut = CachedData::from(collect($contents));
$this->assertNull($sut->lastModified());
}
/**
* @dataProvider dateTimeProvider
*/
public function testLastModifiedReturnsModifiedTime($now, $dateTime)
{
$sut = CachedData::from(collect(["modifiedAt" => $dateTime]));
$this->assertEquals($now->getTimestamp(), $sut->lastModified());
}
public function testIsEmptyReturnsTrueIfEmpty()
{
$sut = CachedData::from(collect());
$this->assertEquals(true, $sut->isEmpty());
}
public function testIsExpiredReturnTrueIfEmpty()
{
$sut = CachedData::from(collect());
$this->assertEquals(true, $sut->isExpired());
}
}

View File

@ -1,13 +1,12 @@
<?php <?php
/** @noinspection PhpIllegalPsrClassPathInspection */
namespace Unit; namespace Tests\Unit;
use App\Anime; use App\Anime;
use App\Contracts\Repository; use App\Contracts\Repository;
use App\Profile; use App\Profile;
use App\Services\DefaultCachedScraperService; use App\Services\DefaultCachedScraperService;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Jenssegers\Mongodb\Eloquent\Builder; use Jenssegers\Mongodb\Eloquent\Builder;
use Jikan\MyAnimeList\MalClient; use Jikan\MyAnimeList\MalClient;
use JMS\Serializer\SerializerInterface; use JMS\Serializer\SerializerInterface;
@ -17,8 +16,9 @@ use Tests\TestCase;
final class DefaultCachedScraperServiceTest extends TestCase final class DefaultCachedScraperServiceTest extends TestCase
{ {
public function tearDown(): void protected function tearDown(): void
{ {
parent::tearDown();
// reset time pinning // reset time pinning
Carbon::setTestNow(); Carbon::setTestNow();
} }
@ -35,8 +35,9 @@ final class DefaultCachedScraperServiceTest extends TestCase
$serializerMock = Mockery::mock(SerializerInterface::class); $serializerMock = Mockery::mock(SerializerInterface::class);
$repositoryMock $repositoryMock
->expects() ->allows()
->where("request_hash", $this->requestHash()) ->where("request_hash", $this->requestHash())
->atMost()
->times($repositoryWhereCallCount) ->times($repositoryWhereCallCount)
->andReturn($queryBuilderMock); ->andReturn($queryBuilderMock);
@ -240,6 +241,7 @@ final class DefaultCachedScraperServiceTest extends TestCase
]); ]);
[$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(); [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks();
// stale record in db
$repositoryMock->expects()->getAllByMalId($malId)->andReturns(collect([ $repositoryMock->expects()->getAllByMalId($malId)->andReturns(collect([
$mockModel $mockModel
])); ]));
@ -294,7 +296,7 @@ final class DefaultCachedScraperServiceTest extends TestCase
]); ]);
[$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(); [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks();
$repositoryMock->expects()->where("username", $username)->andReturns($queryBuilderMock); $repositoryMock->expects()->where("username", $username)->atMost()->times(2)->andReturns($queryBuilderMock);
// nothing in db // nothing in db
$queryBuilderMock->expects()->get()->andReturns(collect()); $queryBuilderMock->expects()->get()->andReturns(collect());
// scrape returns data // scrape returns data
@ -311,4 +313,46 @@ final class DefaultCachedScraperServiceTest extends TestCase
$this->assertEquals($mockModel->toArray(), $result->toArray()); $this->assertEquals($mockModel->toArray(), $result->toArray());
} }
public function testIfFindByKeyUpdatesCache()
{
$malId = 1;
$username = "kompot";
$testRequestHash = $this->requestHash();
$now = Carbon::now();
$mockModel = Profile::factory()->makeOne([
"mal_id" => $malId,
"username" => $username,
"modifiedAt" => new UTCDateTime($now->sub("3 days")->getPreciseTimestamp(3)),
"createdAt" => new UTCDateTime($now->sub("3 days")->getPreciseTimestamp(3))
]);
$now = Carbon::now();
Carbon::setTestNow($now);
$updatedMockModel = Profile::factory()->makeOne([
...$mockModel->toArray(),
"location" => "North Pole",
"modifiedAt" => new UTCDateTime(Carbon::now()->getPreciseTimestamp(3)),
"createdAt" => new UTCDateTime(Carbon::now()->getPreciseTimestamp(3))
]);
[$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks();
// stale record in db
$repositoryMock->expects()->where("username", $username)->atMost()->times(3)->andReturns($queryBuilderMock);
$queryBuilderMock->expects()->get()->andReturns(collect([
$mockModel
]));
$repositoryMock->expects()->scrape($username)->andReturns(
collect($updatedMockModel->toArray())->except(["request_hash", "modifiedAt", "createdAt"])->toArray()
);
// mock out update
$queryBuilderMock->expects()->update(Mockery::any())->andReturns($malId);
// second call to ->get() should return the updated value
$queryBuilderMock->expects()->get()->andReturns(collect([
$updatedMockModel
]));
$sut = new DefaultCachedScraperService($repositoryMock, new MalClient(), $serializerMock);
$result = $sut->findByKey("username", $username, $testRequestHash);
$this->assertEquals($updatedMockModel->toArray(), $result->toArray());
}
} }

View File

@ -0,0 +1,37 @@
<?php
/** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\Unit;
use App\Contracts\DataRequest;
use App\Contracts\RequestHandler;
use App\Dto\AnimeSearchCommand;
use App\Support\DefaultMediator;
use Tests\TestCase;
final class DefaultMediatorTest extends TestCase
{
public function testSendShouldReturnErrorResponseIfHandlerClassDoesntExist()
{
$mockHandler = \Mockery::mock(RequestHandler::class);
$mockHandler->allows()->requestClass()->andReturns(AnimeSearchCommand::class);
$sut = new DefaultMediator($mockHandler);
$response = $sut->send(\Mockery::mock(DataRequest::class));
$this->assertEquals(500, $response->status());
}
public function testSendShouldCallHandleOnHandlerIfFound()
{
$mockHandler = \Mockery::mock(RequestHandler::class);
$mockRequest = \Mockery::mock(DataRequest::class);
$mockHandler->allows()->requestClass()->andReturns(get_class($mockRequest));
$mockHandler->expects()->handle($mockRequest)->once()->andReturns(response()->json([
"message" => "success"
]));
$sut = new DefaultMediator($mockHandler);
$response = $sut->send($mockRequest);
$this->assertEquals(200, $response->status());
$this->assertEquals(json_encode(["message" => "success"]), $response->content());
}
}