So... you know you should write unit tests (generally). You know, you should make sure your stuff works before you push, needing often some functional testing as well (I hope, although I should know better than to count on everyone doing this...).
Why would you? You KNOW it's working, right? You tested it, right?
Wrong. You checked a few cases. "Isn't that the same?" Nope... well, kind of, but nope.
When you run your function with an argument you know should pass, that is a happy path check. To me, it becomes a test, when you intentionally picked that value, when you know why this and not that value is worth checking. In a test like this (we can talk about other test approaches in another post) you should first write down what EXPECTED RESULT for it is.
Test, obvi, but... don't just check some cases, think about what values, what steps and what effects you are checking for. Make the testing intentional. After a short while, you will see that it takes as much time than just choosing random values, but it makes a difference that you find issues before anyone else can see you introduced them... because you will eliminate them before they get a chance to notice!
Just write down a list of tests (or go TDD on it) that you'll run:
<tested_function> - <precondition> - <tested_value> - <expected_result>
<precondition> also called "initial conditions" or "setup". All the things you need in place to be even able to start testing.
<tested_value> usually the one thing you are testing in a given test case.
<expected_result> is a return value or a side effect of the function(ality) when run with the
<test-value> passed to it.
Choose test values based on your knowledge of the intended range of values for the function. There are two basic techniques to define values for testing and they work together pretty well: Boundary Values Analysis and Equivalence Partitioning. Worth reading about them, they might seem very simple (the theory actually is) but their usage goes beyond number ranges.
Knowing those two basic techniques will go a long way towards improving your testing abilities.
Or rather "why my testing is not enough"? Well, in a great majority of cases, unfortunately, people are too focused on making their code work in an intended way, so they become narrow-sighted and don't even realize what possible issues they should be watching out for. That's where TDD, BDD, etc. get useful because when you define the expected result/behavior BEFOREHAND, you don't have the narrow sightedness yet and can analyze all sorts of possible scenarios. Negatives? You can't predict many changes and details that come up while already working on your code, but that's ok :)
Even if you become a great tester yourself, making sure you have a nice set of tests on different levels (see Test Pyramid) and your code is so clean, Uncle Bob would be proud, you will still need someone else's eyes on it. It doesn't have to be a professional tester or QA (although great if it is), but at least someone else than you, someone who is not afraid to say:
"You think I can't find any bugs? Hold my beer".