056.1 Lesson 1
Certificate: |
Open Source Essentials |
---|---|
Version: |
1.0 |
Topic: |
056 Collaboration and Communication |
Objective: |
056.1 Development Tools |
Lesson: |
1 of 1 |
Introduction
There are thousands of software development tools, both open source and proprietary. And why not? Programmers love to develop tools for themselves and their colleagues; it’s only natural that they’d invest a lot of time trying to find a tool that works better, removes some annoyance from their workflow, or makes deployment easier.
This lesson focuses on the development process and explains how various types of development tools fit into it. Few specific tools will be named, because any one of them could possibly be flagged as deprecated or obsolete, and be replaced by a new favorite by the time you read this lesson.
Goals of Development
Programming tools juggle multiple goals that sometimes conflict with one another. Here are some sample development goals:
-
Produce robust, accurate programs.
-
Produce programs that run fast.
-
Produce programs that scale well.
-
Produce programs that are highly adaptable to different types of users, different devices (such as laptops, cell phones, and tablets), and different environments.
-
Speed up the development process.
-
Speed up deployment of newly developed features or bug fixes.
-
Reduce tedious work, as well as the number of clerical programming errors that slip through to the test stages of the program.
-
Support legacy code and devices that the organization has trouble replacing.
-
Work together with other familiar tools.
-
Allow for easy roll-back in case of errors or changes in plans.
These goals, and others, lead to the processes described in this lesson and the resulting development tools.
General Development Processes
This section contrasts two general models of historical importance: the waterfall model (introduced in an earlier lesson) and continuous integration/continuous delivery (CI/CD).
Waterfall Model
Although the waterfall model is still widely used, especially by large organizations, it has fallen out of favor among many developers. This model was popular from the 1950s through the 1970s. Elements of the model can still be widely found today, such as formal definitions of requirements and testing programs just before their release (quality assurance).
Even at the time that the term “waterfall” was coined for this model, it had become widely discredited. Problems included:
-
Changing requirements was difficult once development started.
-
Requirements and designs were often misunderstood, because plain-language text can be ambiguous. This problem led to products that didn’t meet the intended requirements, and took months to fix.
-
The process was slow. A new release might be achieved only once a year, or even less often.
-
Bugs caused by the interaction of different modules were hard to catch until late in the process, leading to further delays.
-
The process put up barriers between different parts of the organization, which was bad for the quality of the product as well as organizational esprit de corps.
Principles of Continuous Integration/Continuous Delivery (CI/CD)
The waterfall model was challenged in the 1970s by a series of movements that led to the most celebrated (if not universally used) model today: CI/CD. Stages in the adoption of the CI/CD practices include the Agile Manifesto, released in 2001, SCRUM, and DevOps. The new model is founded on the principles of tight communications among team members, involvement by the end user or customer, rapid roll-out of bug fixes and new features, and rigorous ongoing testing to preserve quality in a fast-moving — sometimes even chaotic — environment.
CI/CD automates as many steps as possible in the stages that range from writing code to deploying the completed program to end users. CI/CD requires defining each step formally and replacing a human action (such as installing software by hand) with a procedure run by a program. Thus, CI/CD is part of a movement known by the phrase “everything as code” and “infrastructure as code.”
Furthermore, once a team has incorporated its processes into code, these processes can be fixed, upgraded, and recorded historically just like code. Anything written by the team, whether programs, automated procedures, or documentation, should be stored in a version control system, which is described in an upcoming lesson.
When several procedures are automated, they can run in sequence. Thus, teams often talk about a CI/CD pipeline, which means that one successful step in the pipeline automatically triggers one or more subsequent processes. A pipeline must be monitored in an automated fashion so that any failure along the way causes the pipeline to stop and notify team members of the failure.
Although the following sections discuss CI and CD separately, they tend to be combined, and the dividing line between them is blurry.
Continuous Integration (CI)
Continuous integration refers to the fast incorporation of small changes into the code. The central repository used to create the product for end-users is known as the core repository (or core repo). Each programmer creates a personal workspace (often called a sandbox) on their computer and pulls from the core repository the files they need to fix or upgrade.
The programmer could use a personal laptop or desktop (known as a local development system), downloading relevant parts of the core repository and then uploading changes after local testing. Alternatively, the programmer could take advantage of the popular trend called “cloud computing” and work on a shared system run by their organization, a remote development system.
The programmer generally checks for errors using a debugger, a separate program that runs the programmer’s work in a controlled environment. We’ll look later at debuggers and other tools for catching errors.
To catch errors as early in the development process as possible, the programmer also runs tests on the code before uploading it to the core repository. These are called unit tests because each focuses on one tiny element of the code: For instance, did a function increment a counter as it was supposed to?
The last stage in CI is checking the programmer’s changes into the core repository. At this stage, tools run integration tests to make sure the programmer hasn’t broken anything in the project as a whole.
No matter how far automation has come, some expert member of the team should intervene at the point of integration to make sure the change is desired by the team. Automated tests can determine that nothing has broken, and even whether the change creates the desired effect in the program. But these tests can’t check for all the principles considered important by the team.
Therefore, before proceeding to deployment, a team member should check security, adherence to coding standards, proper documentation, and other principles. (Not surprisingly, tools exist for some of these tasks too; we’ll look at them later in this lesson.)
A lot of testing takes place during CI, and it has to happen both fast and reliably to support the pace of modern development. Therefore, modern tools for integrating code and running tests are automated. A programmer should be able to run a whole suite of unit tests through a single command or a click of a button.
Generally, a partial or full integration test runs whenever the programmer uploads code to the core repository. Any errors caught by the tests can be reported quickly to the programmer.
Continuous Delivery (CD)
As explained in the previous section, CI consists of automated procedures that lead up to a new version of the program in the core directory. Continuous delivery refers to anything that happens after that stage to bring the new version out into the field where end users can benefit from it. The D in CD is sometimes expanded to “development” or “deployment” in addition to “delivery.”
The main tasks of CD are to test the code thoroughly and to upload it to the users' computers or an application repository.
The CD phase runs a battery of tests to provide maximum assurance that the product works well. A larger set of integration tests might be run, along with a number of other tests to determine impacts on the users, performance, and security. A later section in this lesson describes some of these tests.
Testing should be done on different systems from the production systems that serve users. Not only could a catastrophic flaw bring down a production system; it might corrupt user data. Furthermore, tests compete for system time and degrade performance for users.
So for testing, it’s best to set up a complete computing environment that mirrors your production environment, with similar hardware and software. The intermediate environment where testing runs before deployment is often called a staging environment.
Of course, it wouldn’t be practical to make the test environment the same size as the production environment, but the essential elements of the production environment (such as databases) should be included.
One requirement of CD automation is to distinguish between the test and production environments. All the code installations and processes should be tailored to the target environment.
CD offers the most potential for rapid development when the code is a service running on the organization’s own systems. If you’re a retail site, for instance, offering an interactive web page, you have access to all your web servers. You can update them several times a day, if you want, and roll back changes quickly if they turn out to produce problems for users.
Common Software Development Tools
These sections present common types of tools used by programming teams at three levels: code generation, testing, and deployment.
Compilers
A fundamental programming tool is the compiler, which turns very sophisticated, high-level programming languages into the instructions running on the computer processor.
Computers run machine code, which consists of strings of bits (ones and zeros) that the processor turns into instructions and data: loading a value from memory into a register, adding the values of two registers, etc. Processors are distinguished by the different formats and sets of instructions they have, so they have different machine code as well. Vendors try to invent new processors that use the same machine code in order to allow customers to run their old programs on the new processors. When vendors do so, we speak of families of processors.
The next stage in the early years of programming was assembly language, which provided human-readable terms, such as ADD, for each instruction. Programmers wrote in assembly language and submitted their code to a tool called an assembler to translate the assembly language into machine code. Machine code is also called machine language.
Then, higher level languages were created. Nowadays, you can create complex control flows without even specifying their details; you can just indicate your desired results. These programs require a compiler to turn source code into machine code. The compilers can perform quite intelligent transformations and optimizations.
Many languages have multiple compilers available. You can find a choice of compilers for very popular languages, such as C and Java. In the free and open source community, most people use either the GNU Compiler Collection (GCC) or the LLVM compiler for C and C++.
Originally, a compiler would compile each file of source code to produce an intermediate object file, then combine all the object files into one program by invoking another tool called a linker. Modern compilers can compile multiple files at once in order to perform optimizations that cross the boundaries of files.
Compilers used to compile into assembly language, which could be useful because each type of computer processor supported a different machine code but may support the same assembly language. There are multiple variants of assembly languages. The last step in compilation and linking was to produce machine code. As with linking, modern compilers know how to assemble the code into machine code.
But there is another stage in many modern languages: intermediate code, usually called byte code. This code has been compiled into a binary format that is high-level enough to be portable. For instance, the Java language is compiled into byte code so that the program can be loaded onto many different types of processors.
In producing byte code from source code, the compiler has done much of the work. Each computer then hosts its own version of a program called a virtual machine that completes the transformation from byte code into a set of instructions that the virtual machine can execute. Sometimes, byte code is compiled into machine code, too. Going from byte code to a set of instructions is more convenient for end users than going from a high-level programming language to machine code. This was a great advantage for Java when it was invented, because its designers wanted it to run within a virtual machine plug-in for web browsers, where the user’s processor and operating environment could be quite varied.
Java designers promoted the benefits of byte code through the marketing phrase “Compile once, run anywhere.” Some other advantages of byte code emerged later. New languages could be created that provided a very different experience to programmers (hopefully making the job of programming easier and producing more maintainable code) while creating the same byte code that Java virtual machines supported. Functions from these languages were easy to mix into existing Java programs.
Finally, there are some languages, such as Python, for which you don’t have to compile source code at all: You simply enter statements into a processing tool called an interpreter, which converts the code directly into machine code and executes it. Interpreters are slower than compilers, but some have become efficient enough that they don’t impose much of a performance penalty. Still, some popular libraries in the Python language include functions that are accessible to developers in the interpreted language but are programmed in the C language, to speed up execution.
Many interpreted languages provide compilers too. They produce either byte code or machine code, which then executes more quickly than the interpreter.
There are build tools to help programmers manage files and functions. A complex program might contain hundreds of files, and the programmer will need to compile different combinations of files using different compiler options at different times (for instance, to support debugging). A build tool lets the programmers store different options and combinations of files, and easily choose the desired type of build. Maven and Gradle are common build tools for Java and related languages.
Code Generation Tools
The programmer doesn’t have to start coding with a blank screen. Recently, services have sprung up that generate code based on your plain-text description of what you want. This a form of generative AI, and like other such services, some people complain that it draws on the work of former programmers without compensation and produces less robust source code (so far). Aside from ethical controversies, many programmers say that automatic code generation has greatly improved their productivity.
One form of code generation that has been available in many programming languages for many years is refactoring. It examines a large program, which can easily evolve over time into a tangle of files and functions. Refactoring moves functions around to create a more logical structure to the program in the pursuit of improved maintainability and reduced duplication of source code.
Some programmers are tasked with reproducing the functioning of other code. They may need to write a new program to replace a legacy program with missing source code that must be retired. Or they might be mimicking a competitor’s program. This kind of research is called reverse engineering. One useful tool for this purpose is a disassembler, which turns machine code into assembly language. There are also disassemblers for byte code.
Debuggers
Most programmers spend more time debugging than coding. People just don’t think perfectly logically, and therefore forget some detail that the computer requires to run the program the way you want. Thus, your code is likely to fail at first try, and you can benefit greatly from a debugger to uncover the error.
A debugger supports intensive research efforts that can cut hours off of the process for finding errors.
A programmer can ask the program to stop at some key place in the program (a breakpoint), such as the beginning of a function or loop. The debugger can display the values of variables and even computer registers at the current point in the program. The programmer can also run each statement, one at a time, and see the results (single stepping). If the programmer wants to see when and how a variable changes during the run, they can set a watchpoint.
The most prominent debugger in the free and open source world, particularly for C and C++, is the GNU debugger, which works with the GNU compiler mentioned earlier. Other languages also have dedicated debuggers.
Analytical Tools
Although debugging can usually uncover bugs fairly quickly, it’s better to eliminate spelling errors and other basic problems earlier in the programming process. Many ingenious analytical tools exist to check a program. Static analysis examines the code of a program. Dynamic analysis runs a program and checks for problems during its execution.
One of the earliest forms of static analysis was called a linter. It can detect, for instance, if you assign the value of a floating-point variable to an integer. This might or might not produce problems, and might or might not be caught by the compiler. In production, it could lead to incorrect results.
Nowadays, most compilers can do the job of a linter. Some compilers, such as the one for the Rust language, are notable for their strictness in rejecting poorly written code.
Many other types of analytical tools run separately from the compiler. For example, security analysis tools can find problems that make a program vulnerable to hacking. A common error by developers, for instance, is when their code calls a function and doesn’t check whether that function returned an error.
Integrated Development Environments (IDEs)
Many programmers use text editors to enter and edit their code. Text editors are different from word processors, which introduce a lot of formatting (such as italic and bold, bullets and numbered lists, etc.). The text processor produces unadorned text, which programming languages require.
There are also sophisticated tools dedicated to helping programmers develop their programs. These tools are alert to the programming language in use. For instance, if you start to type the name of a variable of function, the tool can suggest a completion. These tools can check for errors while you’re coding, format the code in a consistent and pleasing way, run analytical tools and debuggers, compile the code, handle check-ins to version control systems. and take care of other tasks for you. Therefore, they are called integrated development environments (IDE).
Eclipse is a popular open source IDE.
Common Types of Software Testing
Testing is a major part of software development. Programmers run unit tests as they develop the code. Other types of testing typically run when a programmer checks code back into the core repository, or during deployment.
Unit Testing
We’ve seen that a programmer should take great care to find errors before submitting code for integration into the core repository. Unit tests are crucial to catching errors.
Writing these tests is both an art and a science, and the volume of test code might exceed the volume of production code. There is even a development model called test-driven development (TDD), where programmers write tests before writing the code they want to test. Proponents of TDD claim it plugs gaps in testing and helps ensure that the code does what the programmer wants it to do.
It’s important to test for things that go wrong during program execution, as well as things that go right. If the user, or another part of the program, submits invalid input to a function, it’s important for the function to catch the problem and report an appropriate error message.
JUnit is a popular open source tool for running unit tests on Java programs.
Integration, Regression, and Smoke Testing
While unit tests focus on the individual actions of particular functions, the team should also run tests at a higher level to make sure the product, as a whole, works properly. The tests generally imitate user behavior. For instance, in a restaurant management application, tests could check whether the user gets the item they requested.
When a change to a program breaks some function that worked before, the failure is called a regression, and the tests are called regression tests.
As a team prepares a product for release, the first stage of testing is often very short. The team checks for the most important activities performed by a program and stops testing if any errors arise, thus saving time. This kind of testing is called a smoke test, because an application that malfunctions so easily is like a bad device that catches on fire.
Some products involve user interaction; web and mobile applications are common examples. Therefore, tools have been created to emulate user interactions. The test runs automatically, triggering the code that would have run if a user pressed the button. Selenium is a popular tool in this category.
Acceptance Testing
Programs with a user interface need an extra level of testing beyond proving that they react properly to certain inputs. The programs must also look right on the screen. Acceptance testing checks the impact of the program on the user. Quality assurance teams generally run these tests after integration and regression testing demonstrate that the program is formally correct and meets user expectations.
Security Testing
Security is obviously critical, and even a tiny security failure can expose an organization to major damage. We’ve seen that programmers can run analytical tools to check the security of the code. In the quality assurance stage, tests can also determine whether the program has vulnerabilities. The tests run the program with malicious inputs and make sure the program rejects the input without failing, performing unintended actions, or revealing sensitive information.
One kind of test that has proven valuable in some situations is fuzz testing. The test framework simply generates strings of random garbage and submits them as input to the program. This might seem to be a waste of time, but often turns up vulnerabilities that ordinary testing does not.
Performance Testing
After the program is judged by other tests to work correctly, teams should determine whether it runs fast enough. Performance testing requires an environment similar to the ones where the users will interact with the program. For instance, if users will submit requests from a long distance over a network, performance testing should also be done over a long-distance network.
Some programming libraries are tested through benchmarks: standard tests used to compare different libraries or different versions of the same library.
Common Deployment Environments
A CI/CD tool allows sophisticated ways to construct pipelines. Like computer programs, a pipeline can contain tests and branches. By branching, you can perform one set of activities in the test environment and another in the production environment. You can use the tool to automatically install the correct database or other software needed for different programs.
CD overlaps with DevOps. In cloud environments, CD and DevOps tools create the virtual computer systems in an automated fashion with all the components needed for the programs to run. Automated tools (sometimes called orchestration tools) check for the failures of virtual systems and automatically restart them.
The main job of the CD tool is to launch and step through each pipeline. The tool checks the results of each stage of the pipeline, and chooses whether to proceed or stop. The tool also allows scheduling, and logs its activities.
Often you have to run a task repeatedly with minor variations. For instance, teams distinguish between deploying to a test environment and deploying to production. Therefore, CD tools provide parameters that you can fill in with different values when you run the pipeline.
Sometimes, a process requires commands that traditionally were entered at the terminal. Thus, a CD tool provides mechanisms to run arbitrary commands. Usually, it provides hooks such as preprocess
to run commands before a stage of the pipeline and postprocess
to run commands after a stage of the pipeline.
Jenkins is probably the most popular open source tool for the orchestration described in this section.
Guided Exercises
-
What are some ways to check a program’s security?
-
Why would you write multiple unit tests for a single program function?
Explorational Exercises
-
Your team has inherited an old application that runs slowly and is hard to add features to. What are some ways to improve the application, without throwing it out and writing a new one from scratch?
-
In large projects, frequently, Team A wants a feature implemented in a part of the system maintained by Team B, but Team B doesn’t see the feature as a priority. How can Team A code the feature as part of Team B’s project?
Summary
This lesson has laid out the types of tools used throughout development: compilers and other code generation tools, analyzers, tests, and CI/CD tools that automate integration and delivery. There are many options for each of these activities, and a tool that is popular today may be replaced in a year. Understanding how these tools all fit together in the development process helps you identify what you need.
Answers to Guided Exercises
-
What are some ways to check a program’s security?
First, human experts can examine the code.
Many static and dynamic analysis tools exist to catch poor programming practices that expose a program to security threats.
Other tools submit malicious input to running programs and check their reactions.
-
Why would you write multiple unit tests for a single program function?
A program function usually has to run on many varieties of input, and each variety deserves its own test. For instance, the function might handle an input value of zero in a special way. You also need to anticipate invalid input and write tests to show that the function handles it appropriately.
Answers to Explorational Exercises
-
Your team has inherited an old application that runs slowly and is hard to add features to. What are some ways to improve the application, without throwing it out and writing a new one from scratch?
First, make sure the project is under version control, if it wasn’t placed there already.
After adding a new feature, run regression tests to determine where the program fails, and assign programmers to figure out what functions are responsible. These functions can be selectively replaced.
Performance testing can identify particular functions that run slowly, so you can focus your efforts on fixing or replacing the most inefficient code.
If the code is in a language that is no longer popular, consider adding features in a language preferred by the team. Make sure that functions in the new language can be integrated with the old functions.
-
In large projects, frequently, Team A wants a feature implemented in a part of the system maintained by Team B, but Team B doesn’t see the feature as a priority. How can Team A code the feature as part of Team B’s project?
Team B can allow Team A to create a new branch, code the feature, and submit the branch to Team B to merge into its project. Team A must not be empowered to do anything they want, however. Team B should provide documentation and help for Team A to follow project standards. A member of Team B must also review Team A’s submission, and run all the usual forms of integration testing.
This form of collaboration is sometimes called InnerSource, because it resembles open source but takes place within a single organization.