DEV Community

Cover image for Solving the Problem Sets of CS50's Introduction to Programming with Python — One at a Time: Problem Set 5
Eda
Eda

Posted on • Originally published at rivea0.github.io

Solving the Problem Sets of CS50's Introduction to Programming with Python — One at a Time: Problem Set 5

Read the original blog post here.


It might be true that testing code sometimes seem like a waste of time to a novice programmer. It may seem that you have to put extra effort to write some tests for your code to see if it is working properly. Maybe you have already done some "testing" on your own, plugging different variables here and there. So, why should you even spend another slice of your time to write tests? Well, if you have such thoughts, get ready to appreciate the value of writing tests, because, on this week on Unit Tests, we have some exciting problems to solve.

As it is pointed out in the lecture, the earlier you get into the habit of testing your code, the better. Unit tests kind of tell a story about your program, and how it should work. Writing good tests not only makes your program more robust, but it is also an indication that you have precisely defined how your program should behave and what to expect.

This week, we are visiting some old problems we have solved throughout this course to write tests for them. Instead of using Python's built-in unittest module, we are going to use the beautiful pytest library to implement our tests. Actually, you are already given hints weeks ago to solve all the problems of this week, so, in this post, there will be shorter paragraphs under each problem's header — except for the first one, which we shall see the crux of this week's problem set.

As always, I assume you have read the problem explanations, and I have to give the disclaimer that these posts are intended as a guide, or, (like today) just musings about the problem sets. Let's begin.

Testing my twttr

In this problem, we are reimplementing Setting up my twttr from Problem Set 2 to write some tests for it. Since we have already seen how that problem could be solved, there is no need to go over it again. We need to create two files, called twttr.py and test_twttr.py, inside a new directory called test_twttr.
We only need to restructure our code if it is different from what is given in the problem explanation, separating shorten and main functions.

It is kind of like you are writing check50 for your own code, and, check50 checks your own check50!

Now, how can we go about thinking of what kinds of tests to implement? To be honest, the answer to that question is hidden inside the original problem descriptions, specifically, the explanations of how to test them. However, there might be an issue: the tests that you write must catch the same bugs that the staff version looks for. So, this week, check50 is the kind of the hint itself: Its tests are actually the edge cases that you should be considering. It is the ultimate test to test your tests! Pretty sure, the CS50 staff wrote tests on their own to check check50 as well... But, that is another thing to think about later. For this week, the main thing to consider is to implement the original tests from the original problems themselves. So, if that original problem's check50 was testing certain inputs, you should create your tests similar to those tests that once tested your code for that problem.

I know, it was painful to read and perhaps hard to wrap your mind around, but hopefully the idea is clear. It took me longer to figure that out!

So, in the first problem, we know that the shorten function expects a str, so, we can test it by asserting that for a given string, our function outputs the vowel-stripped version of it. I cannot outright give a hint of what other tests should look like, but you are already given the answer three weeks ago.

The main thing to do here is to write simple assert statements inside our test functions. Say, we want to test if a cast_spell function works:

# 📁 spells.py

def cast_spell(incantation):
    return f'{incantation.upper()}!'
Enter fullscreen mode Exit fullscreen mode

A test for it would be like:

# 📁 test_spells.py

from spells import cast_spell

def test_cast_spell():
    assert cast_spell('lumos') == 'LUMOS!'
    assert cast_spell('expecto patronum') == 'EXPECTO PATRONUM!'
Enter fullscreen mode Exit fullscreen mode

Also, we do not need any kind of try...except, because pytest generously takes care of that. Let's now take a look at the next one.

Back to the Bank

Again, in this problem, we are reimplementing another past problem, namely Home Federal Savings Bank. It was an easy and fun problem to solve, as we have done it before.

We know that the value function should return an int. And, for the tests? Well, the original problem explanation literally tells you that. Three kinds of tests (literally given to you before) — with two variations (for uppercase and lowercase) — should suffice.

Re-requesting a Vanity Plate

In this problem, we are visiting the good old Vanity Plates from Problem Set 2. We have already went through it before. The main thing is to use the kinds of tests and inputs that the staff used to check our problem — which is also given in the original problem explanation. If they are not enough, well, remember there was also a test for the full alphabetical string — which should be valid as long as its length is within the limits.

Refueling

In the last problem of this week, we go back to Fuel Gauge, which we have seen before on the week on Exceptions. Again, we need to implement the same kind of tests as the check50 for the original problem, and the cases to consider are clear in the original problem explanation once more. The new thing here is handling the exceptions with pytest. As always, the documentation itself clearly shows you how to do that. For example, we can look at how to handle a ValueError in our cast_spell function:

# 📁 test_spells.py

import pytest

from spells import cast_spell

def test_valid_type():
    with pytest.raises(ValueError):
        cast_spell(62442)
Enter fullscreen mode Exit fullscreen mode

Now, pytest will make sure that passing 62442 as the input to cast_spell function results in a ValueError. And, that is pretty much it for this problem as well.

I know, this week was a bit confusing. I cannot give many hints this week, but you have a better place to go for hints: check50 itself for the original problems. The CS50 staff has already considered many edge cases, so, you do not even have to come up with your own examples. You are only implementing the same check50 tests that once checked your own code! It is quite satisfying to think about.

Next week, we are going to dive into the world of File I/O, and perhaps of some exciting libraries as well. Until then, happy coding!

Discussion (4)

Collapse
raphaeluziel profile image
Raphael Uziel

Issue with refueling problem

I've struggled with this Refueling part for the past couple of days. Maybe I'm going about it all wrong, but I just don't get it! Here's the issue:

If, for example I use a try/except statement to catch a ZeroDivision error, the fuel.py works, and passes check50, but pytest gives me a Failed: DID NOT RAISE <class 'ZeroDivisionError'> error, and I guess that is why check50 doesn't get past the :) test_fuel.py exist check.

I read that the except, because it handles the exception raised, will not throw the exception to the caller, which I guess is pytest. Okay, so I change my code so that it raises the ZeroDivisionError instead. Now, pytest passes all tests, but of course, fuel.py does not since instead of prompting the user again if the user inputs '100/0', it will stop the execution of the program, raising the ZeroDivisionError.

My guess is that I'm missing something very basic, and that there is no bug, but if anyone has actually solved this, any help would be appreciated!

Thanks!

Collapse
rivea0 profile image
Eda Author

Hi Raphael,
Like you said, the main reason that pytest gives that error is that you need to raise ZeroDivisionError instead of catching it with try...except. Since you fixed it, I think the error might not be in the test file, but in fuel.py. For example, we need to keep getting user input only inside the main function, so, an infinite loop is reasonable to use there. And, only when x is less than or equal to y you should break out of it. That way, I think it would not stop the execution when it encounters an error. Because the convert function should only do the converting, and gauge should just return a formatted result string, the issue might be inside the main function.

Collapse
raphaeluziel profile image
Raphael Uziel

Thanks so much for your help, Eda, I don't think I would have ever figured this out on my own! I was already doing the while loop inside the main function, but I was catching the exceptions in the convert() function instead of main().

Thread Thread
rivea0 profile image
Eda Author

I'm glad it helped!