DEV Community

Cover image for Freezing Time: Testing Laravel Temporary Storage URLs
Ivan Mykhavko
Ivan Mykhavko

Posted on

Freezing Time: Testing Laravel Temporary Storage URLs

In a previous article, Testing Temporary URLs in Laravel Storage, we explored two techniques to test Laravel's Storage::temporaryUrl() method. While Storage::fake doesn't natively support temporaryUrl, we demonstrated how to use mocking to simulate the behavior effectively. If you haven’t read it yet, consider starting there to understand the foundation of testing temporary URLs in Laravel.

In this continuation, we'll delve into how freezing time can make testing temporary URLs more reliable, especially for time-sensitive functionality. We'll leverage Laravel's built-in test helpers and Carbon's time manipulation features to solve potential inconsistencies in tests.

Why Freezing Time Matters

Temporary URLs often include expiration timestamps, making them time-sensitive. In a test environment, a small delay in execution can cause mismatched expiration times, leading to test failures. For example:

Failed asserting that two strings are equal.
Expected :'http://localhost/test/image?expiration=1737729799'
Actual   :'http://localhost/test/image?expiration=1737729800'
Enter fullscreen mode Exit fullscreen mode

This happens when the expiration timestamp generated during test execution slightly differs due to the passage of time. Freezing time ensures that all time-related operations return consistent values, eliminating such discrepancies.

Laravel's Time Freezing Helpers

Laravel offers several methods for freezing and manipulating time in tests:

  • $this->freezeTime(): Freezes time to the current moment. Any subsequent calls to time-based methods will use this frozen time.
  • $this->travelTo(Carbon::now()): Simulates moving to a specific point in time.
  • Carbon::setTestNow(Carbon::now()): Directly sets the current time for all Carbon operations.

These methods allow you to control the flow of time and ensure consistency across your tests.

For more details, refer to these resources:

Practical Example: External Image Fetching with Temporary URLs

Controller

Here’s a controller method that fetches an image from an external source, stores it in the local storage, and generates a temporary URL for redirection:

final class ExternalImageTMController extends Controller
{
    /**
     * @throws ItemNotFoundException
     */
    public function show(string $path): RedirectResponse
    {
        $path = Str::replace('-', '/', trim($path));

        if (!Storage::disk(StorageDiskName::DO_S3_TEHNOMIR_PRODUCT_IMAGE->value)->exists($path)) {
            $externalUrl = implode('/', [config('services.tehnomir.s3_image_url'), $path]);
            $response = Http::get($externalUrl);

            if (!$response->successful()) {
                throw new ItemNotFoundException('External image', [$path]);
            }

            Storage::disk(StorageDiskName::DO_S3_TEHNOMIR_PRODUCT_IMAGE->value)->put($path, $response->body());
        }

        return Redirect::away(
            Storage::disk(StorageDiskName::DO_S3_TEHNOMIR_PRODUCT_IMAGE->value)->temporaryUrl($path, Carbon::now()->addHour())
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

This method ensures the image is fetched and stored locally if it doesn’t already exist, then redirects the user to a temporary URL for the image.

Testing the Controller

Here’s how you can test the above functionality with time freezing:

public function test_image_is_fetched_externally_stored_and_redirected(): void
{
    // Arrange
    $user = $this->getDefaultUser();
    $this->actingAsFrontendUser($user);

    $this->freezeTime();

    $path = 'test/image';
    $disk = Storage::fake(StorageDiskName::DO_S3_TEHNOMIR_PRODUCT_IMAGE->value);
    $externalUrl = config('services.tehnomir.s3_image_url') . '/' . $path;

    Http::fake([
        $externalUrl => Http::response('external-image-content'),
    ]);

    $disk->assertMissing($path);

    // Act
    $response = $this->getJson(route('api-v2:external.images.tm.show', ['path' => 'test-image']));

    // Assert
    $disk->assertExists($path);
    $this->assertEquals('external-image-content', $disk->get($path));

    $temporaryUrl = $disk->temporaryUrl($path, Carbon::now()->addHour());
    $response->assertRedirect($temporaryUrl);
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  1. $this->freezeTime(): Ensures that all time-based operations in the test use the same frozen time.
  2. Storage Assertions:
    • assertMissing: Verifies the file doesn’t exist before the operation.
    • assertExists: Confirms the file is stored after the operation.
  3. HTTP Fake: Simulates the external API call to fetch the image.
  4. Temporary URL Validation: Compares the expected and actual temporary URLs, which remain consistent due to time freezing.

Without freezing time, this test could fail due to mismatched timestamps in the temporary URL.

Conclusion

Freezing time is a simple yet powerful technique to ensure the reliability of your time-sensitive tests. By combining Laravel’s test helpers ($this->freezeTime) and Carbon’s time manipulation methods (Carbon::setTestNow), you can eliminate inconsistencies caused by execution delays.

By adopting these practices, you’ll have more predictable and robust tests for temporary URLs and other time-sensitive functionality.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay