Skip to main content

30 best practices for software development and testing



Joining any new company—with an established culture and programming practices—can be a daunting experience. When I joined the Ansible team, I decided to write up the software engineering practices and principles I’ve learned over the years and to which I strive to work. This is a non-definitive, non-exhaustive list of principles that should be applied with wisdom and flexibility.

My passion is for testing, as I believe that good testing practices can both ensure a minimum quality standard (sadly lacking in many software products), and can guide and shape development itself. Many of these principles relate to testing practices and ideals. Some of these principles are Python-specific, but most are not. (For Python developers, PEP 8 should be your first stop for programming style and guidelines.)

Programming and development

New Python content
Our latest JavaScript articles
Recent Perl posts
Red Hat Developers Blog
In general, we programmers are an opinionated lot, and strong opinions are often a sign of great passion. With that in mind, feel free to disagree with these points, and we can discuss and debate them in the comments.

Development and testing best practices
1. YAGNI: "You Aint Gonna Need It". Don't write code that you think you might need in future, but don't need yet. This is coding for imaginary future use cases, and inevitably the code will become dead code or need rewriting because the future use case always turns out to work slightly differently from how you imagined it.

If you put code in for a future use case, I will question it in a code review. (You can, and must, design APIs, for example, to permit future use cases, but that's a different issue.)

The same is true for commenting-out code; if a block of commented code is going into a release, it shouldn't exist. If it is code that may be restored, make a ticket and reference the commit hash for the code delete. YAGNI is a core element of agile programming. The best reference for this is Extreme Programming Explained, by Kent Beck.

2. Tests don't need testing. Infrastructure, frameworks, and libraries for testing need tests. Don't test the browser or external libraries unless you really need to. Test the code you write, not other people’s code.

3. The third time you write the same piece of code is the right time to extract it into a general-purpose helper (and write tests for it). Helper functions within a test don't need testing; when you break them out and reuse them they do need tests. By the third time you've written similar code, you tend to have a clear idea of what shape the general-purpose problem is that you're solving.

Also Refer Performance testing here.

4. When it comes to API design (external facing and object API): Simple things should be simple; complex things should be possible. Design for the simple case first, with preferably zero configuration or parameterization, if that's possible. Add options or additional API methods for more complex and flexible use cases (as they are needed).

5. Fail fast. Check input and fail on nonsensical input or invalid state as early as possible, preferably with an exception or error response that will make the exact problem clear to your caller. Permit "innovative" use cases of your code though (i.e., don't do type checking for input validation unless you really need to).

6. Unit tests test to the unit of behavior, not the unit of implementation. Changing the implementation, without changing the behavior or having to change any of your tests is the goal, although not always possible. So where possible, treat your test objects as black boxes, testing through the public API without calling private methods or tinkering with state.

For some complex scenarios—such as testing behavior on a specific complex state to find an obscure bug—that may not be possible. Writing tests first really helps with this as it forces you to think about the behavior of your code and how you're going to test it before you write it. Testing first encourages smaller, more modular units of code, which generally means better code. A good reference for getting started with the "test first" approach is Test Driven Development by Example, by Kent Beck.

7. For unit tests (including test infrastructure tests) all code paths should be tested. 100% coverage is a good place to start. You can't cover all possible permutations/combinations of state (combinatorial explosion), so that requires consideration. Only if there is a very good reason should code paths be left untested. Lack of time is not a good reason and ends up costing more time. Possible good reasons include: genuinely untestable (in any meaningful way), impossible to hit in practice, or covered elsewhere in a test. Code without tests is a liability. Measuring coverage and rejecting PRs that reduce coverage percentage is one way to ensure you make gradual progress in the right direction.

8. Code is the enemy: It can go wrong, and it needs maintenance. Write less code. Delete code. Don’t write code you don’t need.

9. Inevitably, code comments become lies over time. In practice, few people update comments when things change. Strive to make your code readable and self-documenting through good naming practices and known programming style.

Code that can't be made obvious—working around an obscure bug or unlikely condition, or a necessary optimization—does need commenting. Comment the intent of the code, and why it is doing something rather than what it is doing. (This particular point about comments being lies is controversial, by the way. I still think it’s correct, and Kernighan and Pike, authors of The Practice of Programming, agree with me.)

10. Write defensively. Always think about what can go wrong, what will happen on invalid input, and what might fail, which will help you catch many bugs before they happen.

11. Logic is easy to unit test if it is stateless and side-effect free. Break out logic into separate functions, rather than mixing logic into stateful and side-effect-filled code. Separating stateful code and code with side-effects into smaller functions makes them easier to mock out and unit test without side-effects. (Less overhead for tests means faster tests.) Side effects do need testing, but testing them once and mocking them out everywhere else is generally a good pattern.

12. Globals are bad. Functions are better than types. Objects are likely to be better than complex data structures.

13. Using the Python built-in types—and their methods—will be faster than writing your own types (unless you're writing in C). If performance is a consideration, try to work out how to use the standard built-in types rather than custom objects.

14. Dependency injection is a useful coding pattern for being clear about what your dependencies are and where they come from. (Have objects, methods, and so on receive their dependencies as parameters rather than instantiating new objects themselves.) This does make API signatures more complex, so it is a trade-off. Ending up with a method that needs 10 parameters for all its dependencies is good sign your code is doing too much, anyway. The definitive article on dependency injection is "Inversion of Control Containers and the Dependency Injection Pattern," by Martin Fowler.

15. The more you have to mock out to test your code, the worse your code is. The more code you have to instantiate and put in place to be able to test a specific piece of behavior, the worse your code is. The goal is small testable units, along with higher-level integration and functional tests to test that the units cooperate correctly.

16. External-facing APIs are where "design up front"—and consideration about future use cases—really matters. Changing APIs is a pain for us and for our users, and creating backwards incompatibility is horrible (although sometimes impossible to avoid). Design external facing APIs carefully, still keeping to the "simple things should be simple" principle.

17. If a function or method goes past 30 lines of code, consider breaking it up. A good maximum module size is about 500 lines. Test files tend to be longer than this.

18. Don’t do work in object constructors, which are hard to test and surprising. Don’t put code in __init__.py (except imports for namespacing). __init__.py is not where programmers generally expect to find code, so it’s "surprising."

19. DRY (Don’t Repeat Yourself) matters much less in tests than it does in production code. Readability of an individual test file is more important than maintainability (breaking out reusable chunks). That’s because tests are executed and read individually rather than themselves being part of a larger system. Obviously excessive repetition means reusable components can be created for convenience, but it’s much less of a concern than it is for production.

20. Refactor whenever you see the need and have the chance. Programming is about abstractions, and the closer your abstractions map to the problem domain, the easier your code is to understand and maintain. As systems grow organically, they need to change structure for their expanding use case. Systems outgrow their abstractions and structure, and not changing them becomes technical debt that is more painful (and slower and more buggy) to work around. Include the cost of clearing technical debt (refactoring) within the estimates for feature work. The longer you leave the debt around, the higher the interest it accumulates. A great book on refactoring and testing is Working Effectively with Legacy Code, by Michael Feathers.

21. Make code correct first and fast second. When working on performance issues, always profile before making fixes. Usually the bottleneck is not quite where you thought it was. Writing obscure code because it is faster is only worth it if you’ve profiled and proven that it’s actually worth it. Writing a test that exercises the code you’re profiling with timing around it makes knowing when you’re done easier, and can be left in the test suite to prevent performance regressions. (With the usual note that adding timing code always changes the performance characteristics of the code, making performance work one of the more frustrating tasks.)

22. Smaller, more tightly scoped unit tests give more valuable information when they fail—they tell you specifically what is wrong. A test that stands up half the system to test behavior takes more investigation to determine what is wrong. Generally a test that takes more than 0.1 seconds to run isn’t a unit test. There’s no such thing as a slow unit test. With tightly scoped unit tests testing behavior, your tests act as a de facto specification for your code. Ideally if someone wants to understand your code, they should be able to turn to the test suite as "documentation" for the behavior. A great presentation on unit testing practices is Fast Test, Slow Test, by Gary Bernhardt:





23. "Not Invented Here" is not as bad as people say. If we write the code, then we know what it does, we know how to maintain it, and we’re free to extend and modify it as we see fit. This follows the YAGNI principle: We have specific code for the use cases we need rather than general purpose code that has complexity for things we don’t need. On the other hand, code is the enemy, and owning more code than necessary is bad. Consider the trade-off when introducing a new dependency.

24. Shared code ownership is the goal; siloed knowledge is bad. At a minimum, this means discussing or documenting design decisions and important implementation decisions. Code review is the worst time to start discussing design decisions as the inertia to make sweeping changes after code has been written is hard to overcome. (Of course it’s still better to point out and change design mistakes at review time than never.)

25. Generators rock! They’re generally shorter and easier to understand than stateful objects for iteration or repeated execution. A good introduction to generators is "Generator Tricks for Systems Programmers," by David Beazley.

26. Let’s be engineers! Let’s think about design and build robust and well-implemented systems, rather than growing organic monsters. Programming is a balancing act, however. We’re not always building a rocket ship. Over-engineering (onion architecture) is as painful to work with as under-designed code. Almost anything by Robert Martin is worth reading, and Clean Architecture: A Craftsman’s Guide to Software Structure and Design is a good resource on this topic. Design Patterns is a classic programming book that every engineer should read.

27. Intermittently failing tests erode the value of your test suite, to the point in which eventually everyone ignores test run results because there’s always something failing. Fixing or deleting intermittently failing tests is painful, but worth the effort.

28. Generally, particularly in tests, wait for a specific change rather than sleeping for an arbitrary amount of time. Voodoo sleeps are hard to understand and slow down your test suite.

29. Always see your test fail at least once. Put a deliberate bug in and make sure it fails, or run the test before the behavior under test is complete. Otherwise you don’t know that you’re really testing anything. Accidentally writing tests that actually don’t test anything or that can never fail is easy.

30. And finally, a point for management: Constant feature grind is a terrible way to develop software. Not letting developers take pride in their work ensures you won’t get the best out of them. Not addressing technical debt slows down development and results in a worse, more buggy product.

For Original content Refer Here

Comments

Popular posts from this blog

CommVault Web Serach Interface 7.0 for Users

The Business Challenge Data boom maintains to place strain on information technology (IT) infrastructures, traumatic ever increasing budgets and gadget. Unstructured report structures and email data present challenges in growth and control, due to the character of how and whilst the data is created. customers are normally now not aware of quotas, and are normally disorganized in saving and naming their files and in dealing with their email folders. This makes it more tough to decide which files and messages are commercial enterprise-essential and which can be retired to help control data increase. This data chaos additionally makes it difficult for directors to help customers to find lacking or lost files while required. The CommVault® solution CommVault® offers a notably higher approach that gets rid of the tiers of separation typically found between users and their data. in place of requiring users to work through a help table and management groups to find and recover their d...

Packet Tracer vs a Real CCENT or CCNA Lab

So virtualization has come a long way…right?  Sure it absolutely has.  But what about in helping you prepare for your Cisco CCENT or CCNA exam ?  Well you have heard the saying that there is nothing like the real thing…right?  Well I have to agree with it for those of you who are preparing for your CCENT and CCNA exams.  Now let me say that I do feel that there is a place for simulators.  But that is usually reserved for the situations in which someone cannot afford a lab or when you are at a CCNP level and you just need to simulate a certain technology that it may not make sense to spend $500 to see a single concept work.  It is in those situations that the simulator is generally being used by someone who is a senior network engineer that clearly understands the foundation of the CCNA exam and is simply looking to augment a small part of their skill set. EIGRP CCENT & CCNA Lab So let’s bring it back down to a CCNA level.  Why do I frown...

What are The Benefits of Learning Robotic Process Automation?

Robotic Process Automation Introduction: People dependably consider making their life less requesting, snappier and beneficial with improved gainfulness, capability, and ampleness towards dealing with their issues and fulfilling their requirements in a simple way. With the approach of PCs, enormous limit of data and calculations wound up possible with a single mouse click. However, in the meantime, a human was relied upon to work the structure. By and by, the routinely creating advancement will allow taking individuals too out from the condition. Also, this endeavor has accomplished new tops at exponential speeds. We, people, have continually tended to accomplish a frequently extending number of things by the machines and particularly those things which the machines could improve the conditions superior to us. Before Computers showed up, individuals used to do all the constant work with limited means available inside reach. In this article, I will talk about why we are hearing a consid...