Laravel Test Case Exception while using Mockery

76 Views Asked by At

User model

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    const TYPE_MERCHANT = 'merchant';
    const TYPE_AFFILIATE = 'affiliate';

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'type'
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function merchant(): HasOne
    {
        return $this->hasOne(Merchant::class);
    }

    public function affiliate(): HasOne
    {
        return $this->hasOne(Affiliate::class);
    }
}

Merchant model

class Merchant extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'domain',
        'display_name',
        'turn_customers_into_affiliates',
        'default_commission_rate'
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }
}

Affiliate model

class Affiliate extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'merchant_id',
        'commission_rate',
        'discount_code'
    ];

    public function merchant()
    {
        return $this->belongsTo(Merchant::class);
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function orders()
    {
        return $this->hasMany(Order::class);
    }
}

Order model

class Order extends Model
{
    use HasFactory;

    const STATUS_UNPAID = 'unpaid';
    const STATUS_PAID = 'paid';

    protected $fillable = [
        'external_order_id',
        'merchant_id',
        'affiliate_id',
        'subtotal',
        'commission_owed',
        'payout_status',
        'customer_email',
        'created_at'
    ];

    public function merchant()
    {
        return $this->belongsTo(Merchant::class);
    }

    public function affiliate()
    {
        return $this->belongsTo(Affiliate::class);
    }
}

OrderServiceTest Case

class OrderServiceTest extends TestCase
{
    use RefreshDatabase, WithFaker;

    protected Merchant $merchant;

    public function setUp(): void
    {
        parent::setUp();

        $this->merchant = Merchant::factory()
            ->for(User::factory())
            ->create();
    }

    protected function getOrderService(): OrderService
    {
        return $this->app->make(OrderService::class);
    }

    public function test_create_order()
    {
        $data = [
            'order_id' => $this->faker->uuid(),
            'subtotal_price' => round(rand(100, 999) / 3, 2),
            'merchant_domain' => $this->merchant->domain,
            'discount_code' => $this->faker->uuid(),
            'customer_email' => $this->faker->email(),
            'customer_name' => $this->faker->name()
        ];

        /** @var Affiliate $affiliate */
        $affiliate = Affiliate::factory()
            ->for($this->merchant)
            ->for(User::factory())
            ->create([
                'discount_code' => $data['discount_code']
            ]);

        $this->mock(AffiliateService::class)
            ->shouldReceive('register')
            ->once()
            ->with(\Mockery::on(fn ($model) => $model->is($this->merchant)), $data['customer_email'], $data['customer_name'], 0.1);

        $this->getOrderService()->processOrder($data);

        $this->assertDatabaseHas('orders', [
            'subtotal' => $data['subtotal_price'],
            'affiliate_id' => $affiliate->id,
            'merchant_id' => $this->merchant->id,
            'commission_owed' => $data['subtotal_price'] * $affiliate->commission_rate,
            'external_order_id' => $data['order_id']
        ]);
    }

    public function test_process_duplicate_order()
    {
        /** @var Order $order */
        $order = Order::factory()
            ->for(Merchant::factory()->for(User::factory()))
            ->create();

        $data = [
            'order_id' => $order->external_order_id,
            'subtotal_price' => round(rand(100, 999) / 3, 2),
            'merchant_domain' => $this->merchant->domain,
            'discount_code' => $this->faker->uuid(),
            'customer_email' => $this->faker->email(),
            'customer_name' => $this->faker->name()
        ];

        $this->getOrderService()->processOrder($data);

        $this->assertDatabaseCount('orders', 1);
    }
}

This is the test case that I am trying to pass, as this test case is given by the client so I can not modify the test case at all below is the given solution which I have written

OrderService Class

class OrderService
{
    public function __construct(
        protected AffiliateService $affiliateService,
        protected  ApiService $apiService
    ) {}

    /**
     * Process an order and log any commissions.
     * This should create a new affiliate if the customer_email is not already associated with one.
     * This method should also ignore duplicates based on order_id.
     *
     * @param  array{order_id: string, subtotal_price: float, merchant_domain: string, discount_code: string, customer_email: string, customer_name: string} $data
     * @return void
     */
    public function processOrder(array $data): void
    {
        if(!$this->getOrderByExternalOrderId($data['order_id'])){
            $merchant =  Merchant::where('domain', $data['merchant_domain'])->firstOrFail();
            $affiliate = Affiliate::where('user_id', function ($query) use ($data) {
                $query->select('id')
                    ->from('users')
                    ->where('email', $data['customer_email']);
            })->first();

            if (!$affiliate) {
                $affiliate = $this->affiliateService->register($merchant, $data['customer_email'], $data['customer_name'], 0.1);
            }

            $order = new Order([
                'external_order_id' => $data['order_id'],
                'subtotal' => $data['subtotal_price'],
                'affiliate_id' => $affiliate->id,
                'merchant_id' => $merchant->id,
                'commission_owed' => $data['subtotal_price'] * $affiliate->commission_rate,
            ]);

            $order->save();
        }
    }



    public function getOrderByExternalOrderId($externalOrderId){
        return Order::where('external_order_id', $externalOrderId)->first();
    }
}

Affilate Service

class AffiliateService
{
    public function __construct(
        protected ApiService $apiService,
        protected  Affiliate $affiliate
    ) {}

    /**
     * Create a new affiliate for the merchant with the given commission rate.
     *
     * @param  Merchant $merchant
     * @param  string $email
     * @param  string $name
     * @param  float $commissionRate
     * @return Affiliate
     */
    public function register(Merchant $merchant, string $email, string $name, float $commissionRate): Affiliate
    {

        if ($this->getAffiliate($email)) {
            throw new AffiliateCreateException('User already exists');
        }

        $affiliateUser  =  User::create([
            'name' => $name,
            'email' => $email,
            'type' => User::TYPE_AFFILIATE,
        ]);

        $discountCode = $this->apiService->createDiscountCode($merchant);

        $affiliate = $this->affiliate->create([
            'user_id' => $affiliateUser->id,
            'merchant_id' => $merchant->id,
            'commission_rate' => $commissionRate,
            'discount_code' => $discountCode['code'],
        ]);
        Mail::to($email)->send(new AffiliateCreated($affiliate));
        return $affiliate;
    }

    public function getAffiliate($email): ?User
    {
         return  User::where('email',$email)->first();
    }
}

But I am getting the following error


Mockery\Exception\BadMethodCallException : Received Mockery_3_App_Models_Affiliate::getAttribute(), but no expectations were specified
 /Users/mac/Downloads/testProject/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2222
 /Users/mac/Downloads/testProject/app/Services/OrderService.php:43
 /Users/mac/Downloads/testProject/tests/Feature/Services/OrderServiceTest.php:59
0

There are 0 best solutions below