Django testing fails with object not found in response.context even though it works when actually running

Question:

I’m trying to test if my PlayerPoint model can give me the top 5 players in regards to their points.
This is the Player model:

class Player(AbstractUser):
    phone_number = models.CharField(
        max_length=14,
        unique=True,
        help_text="Please ensure +251 is included"
    )

and this is the PlayerPoint model:

class PlayerPoint(models.Model):
    OPERATION_CHOICES = (('ADD', 'ADD'), ('SUB', 'SUBTRACT'), ('RMN', 'REMAIN'))

    points = models.IntegerField(null=False, default=0)
    operation = models.CharField(
        max_length=3,
        null=False,
        choices=OPERATION_CHOICES,
        default=OPERATION_CHOICES[2][0]
    )
    operation_amount = models.IntegerField(null=False)
    operation_reason = models.CharField(null=False, max_length=1500)
    player = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=False,
        on_delete=models.PROTECT,
        to_field="phone_number",
        related_name="player_points"
    )
    points_ts = models.DateTimeField(auto_now_add=True, null=False)

    class Meta:
        get_latest_by = ['pk', 'points_ts']

I also have a pre-save signal handler:

@receiver(signals.pre_save, sender=PlayerPoint)
def pre_save_PlayerPoint(sender, instance, **_):
    if sender is PlayerPoint:
        try:
            current_point = PlayerPoint.objects.filter(player=instance.player).latest()
        except PlayerPoint.DoesNotExist as pdne:
            if "new player" in instance.operation_reason.lower():
                print(f"{pdne} {instance.player} must be a new")
                instance.operation_amount = 100
                instance.points = int(instance.points) + int(instance.operation_amount)
            else:
                raise pdne
        except Exception as e:
            print(f"{e} while trying to get current_point of the player, stopping execution")
            raise e
        else:
            if instance.operation == PlayerPoint.OPERATION_CHOICES[0][0]:
                instance.points = int(current_point.points) + int(instance.operation_amount)
            elif instance.operation == PlayerPoint.OPERATION_CHOICES[1][0]:
                if int(current_point.points) < int(instance.operation_amount):
                    raise ValidationError(
                        message="not enough points",
                        params={"points": current_point.points},
                        code="invalid"
                    )
                instance.points = int(current_point.points) - int(instance.operation_amount)

As you can see there is a foreign key relation.
Before running the tests, in the setUp() I create points for all the players as such:

class Top5PlayersViewTestCase(TestCase):
    def setUp(self) -> None:
        self.player_model = get_user_model()

        self.test_client = Client(raise_request_exception=True)
        self.player_list = list()
        for i in range(0, 10):
            x = self.player_model.objects.create_user(
                phone_number=f"+2517{i}{i}{i}{i}{i}{i}{i}{i}",
                # first_name="test",
                # father_name="user",
                # grandfather_name="tokko",
                # email=f"test_user@tokko7{i}.com",
                # age="29",
                password="password"
            )
            PlayerPoint.objects.create(
                operation="ADD",
                operation_reason="new player",
                player=x
            )
            self.player_list.append(x)

        counter = 500
        for player in self.player_list:
            counter += int(player.phone_number[-1:])
            PlayerPoint.objects.create(
                operation="ADD",
                operation_amount=counter,
                operation_reason="add for testing",
                player=player
            )
            PlayerPoint.objects.create(
                operation="ADD",
                operation_amount=counter,
                operation_reason="add for testing",
                player=player
            )
        return super().setUp()

    def test_monthly_awarding_view_displays_top5_players(self):
        for player in self.player_list:
            print(player.player_points.latest())
    
        # self.test_client.post("/accounts/login/", self.test_login_success_data)
        test_results = self.test_client.get("/points_app/monthly_award/", follow=True)
        self.assertEqual(test_results.status_code, 200)
        self.assertTemplateUsed(test_results, "points_app/monthlytop5players.html")
        self.assertEqual(len(test_results.context.get('results')), 5)
    
        top_5 = PlayerPoint.objects.order_by('-points')[:5]
        for pt in top_5:
            self.assertIn(pt, test_results.context.get('results'))

The full traceback is this after running coverage run manage.py test points_app.tests.test_views.MonthlyAwardingViewTestCase.test_monthly_awarding_view_displays_top5_players -v 2:

Traceback (most recent call last):
  File "/home/gadd/vscodeworkspace/websites/25X/twenty_five_X/points_app/tests/test_views.py", line 358, in test_monthly_awarding_view_displays_top5_players
    self.assertIn(pt, test_results.context.get('results'))
AssertionError: <PlayerPoint: 1190 -- +251799999999> not found in [<User25X: +251700000000>, <User25X: +251711111111>, <User25X: +251722222222>, <User25X: +251733333333>, <User25X: +251744444444>]

This is the view being tested:

def get(request):
    all_players = get_user_model().objects.filter(is_staff=False).prefetch_related('player_points')
    top_5 = list()
    for player in all_players:
        try:
            latest_points = player.player_points.latest()
        except Exception as e:
            print(f"{player} -- {e}")
            messages.error(request, f"{player} {e}")
        else:
            if all(
                [
                    latest_points.points >= 500,
                    latest_points.points_ts.year == current_year,
                    latest_points.points_ts.month == current_month
                ]
            ):
                top_5.append(player)
    
    return render(request, "points_app/monthlytop5players.html", {"results": top_5[:5]})

What am I doing wrong?

Asked By: NegassaB

||

Answers:

I think your problem is with this line:

latest_points = player.player_points.latest()

Specifically, latest(). Like get(), earliest() and latest() raise DoesNotExist if there is no object with the given parameters.

You may need to add get_latest_by to your model’s Meta class. Maybe try this:

class PlayerPoint(models.Model):
    ...

    class Meta:
        get_latest_by = ['joined_ts']

If you don’t want to add this to your model, you could just do it directly:

latest_points = player.player_points.latest('-joined_ts')

if this is the problem.

Answered By: Jarad

There are 2 problems.

  1. In your view, top_5 is an unsorted list of Player objects.
top_5 = sorted(top_5, key=lambda player: player.player_points.latest().points, reverse=True)[:5]  # Add this
return render(request, "points_app/monthlytop5players.html", {"results": top_5[:5]})
  1. In your test, top_5 is a list (actually QuerySet) of PlayerPoint objects.
results_pts = [player.player_points.latest() for player in test_results.context['results']]  # Add this
for pt in top_5:
    # self.assertIn(pt, test_results.context.get('results'))  # Change this
    self.assertIn(pt, results_pts)                            # to this
Answered By: aaron