First and often. If I create some new features for the system, I will first define the interfaces, and then write unit tests for these interfaces. To determine which tests to write, consider the API interface and the functions it provides, pull out a pen and paper and think about possible error conditions or ways to prove that it does the right job. If this is too complicated, most likely your API is not good enough. As for the tests, see if you can avoid writing “integration” tests that test more than one particular object and save them as a “single” test.
Then create a standard implementation of your interface (which does nothing, returns garbage values, but does not throw an exception), connect it to the tests to make sure the tests do not work (this checks that your tests work! :)), Then write to functions and repeat the tests. This mechanism is not ideal, but will cover many simple coding errors and provide you with the ability to launch a new function without having to connect it to the entire application.
After that, you need to test it in the main application using a combination of existing functions. In this case, testing is more complicated, and if possible, it should be partially transferred to a good QA tester, since they will have the ability to break things. Although it helps if you have these skills too. Proper testing is a skill that must be selected in order to be honest. My own experience comes from my naive deployments and subsequent errors that users report when they use it in anger.
At first, when this happened to me, I found it annoying that the user intentionally tried to break my software, and I wanted to mark all the “errors” as “learning issues”. However, after I thought about it, I realized that our role (as developers) is to make the application as simple and reliable as possible, even idiots. Our role is to empower idiots, which is why we are paid for the dollar. Work with idiots.
To test effectively, you must understand how to try to break everything. Assume the mantle of a user who presses buttons and usually tries to destroy your application in strange and beautiful ways. Suppose that if you do not find flaws, then they will be found in production for your companies, a serious loss of face. Take full responsibility for all these problems and curse yourself when the error that you are responsible (or even part of the responsibility) for is discovered in production.
If you do most of the above, you should start creating much more robust code, however it is a bit of an art form and requires a lot of experience to be good.