Mr. Bugayenko talks about the structure of a simple Java project and return values of various methods. In the article, it shows how much work it would take to mock a program that has multiple levels of abstraction and if we did mock everything, it would obscure the meaning of our tests because our tests will would have multiple levels of abstraction and makes it that much harder to see what exactly we are testing. In the end, it would also make the test file much longer than our main file. In situations like this, it would be easier to use fakes.
According to the article linked above, integration testing is a type of software testing that focuses on how well different components of a system interact with each other. For example, when assembling a pen, one would ideally test if all the parts for a pen fit together like the cap, ink cartridge, tail, among other parts. There are two main types of integration testing called unit integrated testing and system integrated testing respectively. Unit integrated testing focuses on testing the interactions and interfaces between integrated components. System integrated testing focuses on testing the interactions and interfaces between systems. There are also four ways to approach integration testing called Big Bang, Top Down, Bottom Up, and Sandwich or Hybrid. Big Bang is when most if not all components are tested at once such as all the functionalities in a system. Top Down is when top-level components are tested first followed by lower-level components. Bottom Up is the opposite where low-level components are tested first followed by higher-level ones. Sandwich, also called Hybrid, is combination of both Top Down and Bottom Up approaches.
For my software capstone, me and three other team members are working on an inventory system. Each of us are working on different components; for example, I am working on the backend API and a messaging system. Right now, I’m in the middle of testing the API by itself, but I’ll have to test it in conjunction with other components like the messaging system, the rest of the backend, the frontend, and also the other systems that are being developed simultaneously. One could call this a form of integration testing, focusing both on the unit and system levels. I feel I’ll take a Hybrid approach since what’ll be tested first is whatever I finish first and what my teammates finish first.
Christian Shadis, CS-443 Self-Directed Blog Post #4
For students like me with limited experience writing JUnit tests, it becomes easy to get lost in the repetition of “Setup, Exercise, Verify” and not realize that a simple, understandable feature of JUnit testing, annotations, can be used to improve the functionality and efficiency of your JUnit Test Suites. In this post I will explain the uses five JUnit 5 annotations and demonstrate them. The following annotations will be explained:
We begin with an extremely simple Java class “Book” and its similarly simple counterpart “BookTest”. See the code for the two classes below.
public class Book {
private int pages;
private String author;
private int pubYear;
public Book(int pages, String author, int pubYear){
this.pages = pages;
this.author = author;
this.pubYear = pubYear;
}
public int getPages(){ return this.pages; }
public String getAuthor(){ return this.author; }
public int getPubYear(){ return this.pubYear; }
public void setPages(int pages){ this.pages = pages; }
public void setAuthor(String author){ this.author = author; }
public void setPubYear(int pubYear){ this.pubYear = pubYear; }
}
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class BookTest {
@Test
void testConstructor(){
Book book1 = new Book(100, "John Doe", 1999);
boolean result = book1 instanceof Book;
assertTrue(result);
}
@Test
void testGetPages(){
Book book1 = new Book(100, "John Doe", 1999);
int result = book1.getPages();
assertEquals(result, 100);
}
@Test
void testGetAuthor(){
Book book1 = new Book(100, "John Doe", 1999);
String result = book1.getAuthor();
assertEquals(result, "John Doe");
}
@Test
void testGetPubYear(){
Book book1 = new Book(100, "John Doe", 1999);
int result = book1.getPubYear();
assertEquals(result, 1999);
}
@Test
void testSetPages(){
Book book1 = new Book(100, "John Doe", 1999);
book1.setPages(150);
assertEquals(book1.getPages(), 150);
}
@Test
void testSetAuthor(){
Book book1 = new Book(100, "John Doe", 1999);
book1.setAuthor("Jane Smith");
assertEquals(book1.getAuthor(), "Jane Smith");
}
@Test
void testSetPubYear(){
Book book1 = new Book(100, "John Doe", 1999);
book1.setPubYear(2001);
assertEquals(book1.getPubYear(), 2001);
}
}
@BeforeEach
The first annotation we will look at is @BeforeEach. This annotation is used for repetitive code that is executed during each test case. If you examined BookTest closely, you might have noticed that the first line of every test case is identical. This makes our suite the perfect candidate to use @BeforeEach. In order to implement, we must create a setUp test method, and put book1 as an instance variable – this way, scope does not become a nuisance. See the revised BookTest.java file below.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class BookTest {
Book book1;
@BeforeEach
void setUp(){
book1 = new Book(100, "John Doe", 1999);
}
@Test
void testConstructor(){
boolean result = book1 instanceof Book;
assertTrue(result);
}
@Test
void testGetPages(){
int result = book1.getPages();
assertEquals(result, 100);
}
@Test
void testGetAuthor(){
String result = book1.getAuthor();
assertEquals(result, "John Doe");
}
@Test
void testGetPubYear(){
int result = book1.getPubYear();
assertEquals(result, 1999);
}
@Test
void testSetPages(){
book1.setPages(150);
assertEquals(book1.getPages(), 150);
}
@Test
void testSetAuthor(){
book1.setAuthor("Jane Smith");
assertEquals(book1.getAuthor(), "Jane Smith");
}
@Test
void testSetPubYear(){
book1.setPubYear(2001);
assertEquals(book1.getPubYear(), 2001);
}
}
@AfterEach
Analogous to @BeforeEach, @AfterEach will execute following each test case. There is no obvious implementation in our code, but I will demonstrate its use nonetheless. We can use @AfterEach to update a count variable indicating how many tests are run. We add a print statement to our setup method to print the test number, and add count as an instance variable, initialized to 0. @AfterEach will allow us to increment the count after each test case is run. Create a method tearDown() with the @AfterEach annotation, and add “count++;” as the body. The count declaration must also be revised to a public static modifier. The new instance variables, setUp(), and tearDown() methods are shown below.
public class BookTest {
Book book1;
private static int count = 0;
@BeforeEach
void setUp(){
book1 = new Book(100, "John Doe", 1999);
System.out.println("Running test " + count);
}
@AfterEach
void tearDown(){
count++;
}
.
.
.
}
We now have methods that execute before and after every single test case.
@BeforeAll
@BeforeAll is used as an initialization method that executes before any other method. It is the first method in BookTest that will be run. We can use it in this example to instantiate our count variable to 0, and leave just the variable declaration with the instance variables. See the modified instance variables and new init() method below.
public class BookTest {
private static int count;
Book book1;
@BeforeAll //runs first
public static void init(){
System.out.println("BeforeAll running: count set to 0.");
count = 0;
}
.
.
.
}
@ParameterizedTest
This annotation is useful for running test cases with multiple inputs. For example, imagine the developer of BookTest is unsure whether testSetAuthor() will pass when multi-word strings are passed as arguments. This scenario is the perfect situation to use a parameterized test. When using @ParameterizedTest, however, one must be sure to specify the source of the arguments, which is accomplished using the annotation @ValueSource. See the example below.
This annotation is used for when tests need to be run multiple times. For example, if we wanted to run testGetAuthor three times, we would use this annotation. When using @RepeatingTest, we must pass a parameter value which determines the number of repetitions, and name which names each repetition. See the example below.
@RepeatedTest(value = 3, name = "Repetition {currentRepetition}")
void testGetAuthor(){
String result = book1.getAuthor();
assertEquals(result, "John Doe");
}
Summary
We have now taken an extremely simple JUnit 5 test suite and used five different annotations to make the suite a bit more complex and comprehensive. There are many more annotations, all of which can be found in the JUnit documentation here. We can now run tests multiple times, create setup and teardown methods, and parameterize tests to run them with multiple argument inputs.
public class Book {
private int pages;
private String author;
private int pubYear;
public Book(int pages, String author, int pubYear){
this.pages = pages;
this.author = author;
this.pubYear = pubYear;
}
public int getPages(){ return this.pages; }
public String getAuthor(){ return this.author; }
public int getPubYear(){ return this.pubYear; }
public void setPages(int pages){ this.pages = pages; }
public void setAuthor(String author){ this.author = author; }
public void setPubYear(int pubYear){ this.pubYear = pubYear; }
}
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class BookTest {
private static int count;
Book book1;
@BeforeAll
public static void init(){
System.out.println("BeforeAll running: count set to 0.");
count = 0;
}
@BeforeEach
void setUp(){
book1 = new Book(100, "John Doe", 1999);
System.out.println("Running test " + count);
}
@AfterEach
void tearDown(){
count = count + 1;
}
@Test
void testConstructor(){
boolean result = book1 instanceof Book;
assertTrue(result);
}
@Test
void testGetPages(){
int result = book1.getPages();
assertEquals(result, 100);
}
@RepeatedTest(value = 3, name = "Repetition {currentRepetition}")
void testGetAuthor(){
String result = book1.getAuthor();
assertEquals(result, "John Doe");
}
@Test
void testGetPubYear(){
int result = book1.getPubYear();
assertEquals(result, 1999);
}
@Test
void testSetPages(){
book1.setPages(150);
assertEquals(book1.getPages(), 150);
}
@ParameterizedTest
@ValueSource(strings = {"Jane", "Jane Smith"})
void testSetAuthor(String str){
book1.setAuthor(str);
assertEquals(book1.getAuthor(), str);
}
@Test
void testSetPubYear(){
book1.setPubYear(2001);
assertEquals(book1.getPubYear(), 2001);
}
}
Although I may not have stated it explicitly, I wrote my previous post regarding using boundary values during testing on programs that are, more or less, complete with all the necessary functions having been implemented. That can be the case for a programmer who is working on software that is relatively small and can be coded to completion by a single individual. Though, one would ask, what happens when the project’s scale is large enough that multiple people would need to be assigned to code certain chunks of code that would be combined into a complete piece of software? The programmers may have some guidelines on or a semblance of how the software should work as a whole, including some of the parts they aren’t actually assigned to implement, yet they may not have the time or resources to work on the other functionalities on top of the ones assigned to them. Yet, to ensure that their assigned functionalities work according to specifications, they will still need the missing code to be implemented for which they may not know how to implement, mostly because of certain specifications that are expected to be met yet are unknown to the programmer. And the cycle continues.
In such cases, one technique that programmers can utilize in order to test their Work-In-Progress code without having to manually implement the missing code is test doubles. Essentially, programmers use test doubles as a way to make up for the missing functionalities by implementing simplified objects or methods such that the can still be run with minimal amount of code that can be easily replaced by the actual final product. Test doubles include, but are not limited to, the following: 1) Dummies 2) Stubs 3) Fakes 4) Mocks
I will be discussing Mocks along with Mockito in more detail in a later post. Instead, in this post I will be focusing on the first three types of test doubles that are given above. What I will mention about mocks, however, is that they can utilize the Mockito testing framework to automate and simplify the process of testing to a more significant degree compared to the previous three types. It goes without saying, however, that each approach has its own merits depending on the needs.
Dummies or dummy objects are objects or procedures with minimal, if any, implementation, making it by far the simplest form of test double implementation. In other words, dummy objects can be considered a bare-bone implementation of code that is sufficient enough to help in testing. Below is a simple example of a dummy object:
public DummyObject(){ } Essentially, the above dummy object is more than enough code even though lacks the implementation. Dummy objects are created to be used when other methods may require parameters, though in this particular case we are not actually creating the objects ourselves.
Stubs are (or I at least believe they are) a more common method of testing methods. Essentially, stubs contain slightly more (yet still bare-bones) implementation than dummies, which is more often than not a predefined return value that corresponds to the return type. Below is an example of a stub method: public double average(double a, double b){ return 3.1; } In the above example, the method will not do any calculations. Instead, it will return an arbitrary value that is relevant to the return type, without any other significance whatsoever. Stubs are used to test if the method actually responds when being called during testing, rather than if the method produces an expected result after running a specific set of computations. I have found myself using stubs when programming in C earlier in my studies just to make sure if other parts of my code can be compiled properly, though now I know that stubs may also be used in unit testing as well.
The last type of test doubles I will be talking about is fakes. Fakes or fake objects have some implementation, though this implementation is often different from that in the end product. Fake objects do not actually use resources (such as data from a database) but may use some arbitrary data in order for the object to be used in testing. An example of a fake object is given below:
Compared to stubs and dummies, fakes contain more implementation, though they are much “lighter” to use than actual objects that contain data that may need to be retrieved in one way or another.
Overall, which of the above implementations may need to be used in testing depends solely on the programmer’s judgement and the state of their code. When combined with other testing techniques (such as boundary value testing), unit testing may provide enough insight to the developers in order to make the appropriate adjustments to their software, be it in regards to fixing faulty code or implementing better optimization. Like I mentioned earlier, each technique has its own merits depending on the circumstances. Moreover, in my next post I will be discussing Mock objects, the Mockito testing framework, and some of the advantages Mockito brings to software testing.
When working on a piece of software or project, often I will encounter a point where some particular piece of the program cannot be tested properly due to one or more components or features not being completed yet. Rather than waiting until all these things are completed, and potentially rushing them to completion in order to do testing, mocking allows me to bypass the issue and use a “mock” version of the missing dependency or component needed for testing.
This is a convenient way to get around having to develop the entire program all at once before being able to test anything, which could arguably lead to more errors or issues over time since you then aren’t able to test as you implement features, but rather only after they have all been implemented. This aspect alone makes mocking seem like an overtly beneficial concept. But there are some drawbacks to consider with mocking, some of which I will discuss below.
In regards to the first issue, repetition, I think that in some cases that this can be justified, and while mocking does represent some duplication in that it is taking a preexisting piece of the program and creating a placeholder for it, this seems unlikely to have the same negative effects that say, having three different train classes which only differ in the value of their color attribute.
The second and third points regarding refactoring and complication of the program seem like legitimate pitfalls to watch out for. When restructuring, the various mocks present could no longer be applicable based on changes in the location of various dependencies and test-cases in relation to them. Mocking does introduce an additional layer of complexity which would not otherwise be present, definitely a fair concern to consider.
Overall, I would say that most of the issues associated with mocking which are described by the author can be mitigated or completely avoided provided that they are considered during development. Mocking still seems in general to be a benefit in relation to testing during development rather than after it has been feature-completed.
As stated in my previous post, I did a group honors project where each member covered a different software testing tool. The primary reason for this was to determine which tools were useful for what purpose, one of which, being Jacoco, I covered in the previous post as well. But, after having heard each group member’s analysis of their tools it begged the question, which tool is best? To properly discuss this I will be examining the pros and cons to each of the tools my group member’s covered in addition to a brief summary of Jacoco.
First there is PIT, which is a mutation testing tool. I was not familiar with these different types of tests, so I will give a quick explanation of what they are. Mutation testing involves the tool essentially making multiple copies of the code which are then given faults, these being the titular mutations. Tests are then run against each mutant with the result being the mutant’s death, meaning the tests failed, or the mutant surviving, meaning the tests passed. The quality of these tests is then determined by the amount of mutants that could be killed. This method of testing is very robust and covers code coverage as well, but this results in these tests being slower than others. PIT is advantageous as it is the fastest of the mutation tests, and has a lot of support from build tools. The other type of testing covered was Programming Mistake Detector, or PMD for short. This tool focuses on finding more expected flaws, like unused variables, useless objects, empty catch blocks, and other such errors we are all familiar with. In addition to this it can also spot any duplicated code with a copy-paste-detector. This is all handled by rules used to test source code, which are grouped into rule sets. Unlike mutation tests, this is much more lightweight and easy to run from a command line, additionally you can run multiple rulesets at once. However this is a less robust testing method, not accounting for any runtime issues or a way to match a ruleset with its generated error easily when running rulesets on a file. So what is the verdict?
Ultimately, there is only really one type of testing that seems sufficient to be used in and of itself, this being mutation testing with PIT. If you are looking for the most condensed testing tool that can do the most in one package, this would be the answer. However you will want to ensure that using mutation testing is necessary, as if you are only really concerned about code coverage Jacoco would be easier and more efficient. Simultaneously, if you want to check the code itself to ensure there are not many errors and do not care about the coverage, you can implement PMD. The answer will ultimately vary from system to system, and if you want to read more about these tools I have provided some resources below. If you want to read about Jacoco be sure to check my previous post!
This week I wanted to look at integration testing. Most projects have multiple components coded by different programmers. The purpose of integration testing is to expose any defects in the way these components interact with each other. It is sometimes called ‘I & T’ (Integration and Testing), ‘String Testing’, or ‘Thread Testing’.
Integration testing is the next level of testing after unit testing. While each individual component of the software is unit tested, there may still be defects. These may be because:
A module written by one programmer may have different logic and understanding of requirements from another causing their two components to work together improperly.
New requirements may be added by the customer that need to be tested.
The software may not be interacting with the database correctly.
There are multiple approaches to integration testing. Big bang testing is integrating all the components at once and testing as a unit. This works well for small systems, but it may be hard to isolate issues. Incremental testing starts with only a few modules ad gradually adds more after testing for proper function. This can be done bottom-up by testing lower-level modules first, or it can be done top-down with higher level modules first. There is also the sandwich method which tests higher level and lower level simultaneously and then combines the two.
While we have not yet taken a look at integration testing in class, I wanted to have a rough idea of what it is at least. I am sure our POGIL activities will go more in depth and explore why integration testing is used as well as exactly how it is done.
Structural testing is based on the source code of the program under test, rather than the definition. This is known as white box testing, while functional testing is known as black-box testing.
Program diagram: For a program written in an imperative programming language, the program diagram is a directed graph with nodes representing statement fragments and edges representing control flow.
DD – path
DD path: Decision-to-decision path (Miller). Begin with the “exit” of the decision statement and end with the “path” of the next decision statement.
DD chain: A path of starting and ending nodes at different points in a directed graph.
Consisting of a node, the internality =0;
Consisting of a node, externality =0;
Consisting of a node, the inner degree > =2 or the outer degree > =2;
Consisting of a node, the degree inside =1 and the degree outside =1;
Length > =1 for maximum drill
DD – path graph
A DD-path graph is a labeled directed graph in which nodes represent the DD Path of a program graph and edges represent the control flow of the Path.
For a given program, many different program diagrams can be constructed, all of which can be reduced to a unique DD-path diagram.
It is possible to generate DD paths for programs up to 100 lines, and above that size, analysis tools are generally required.
The simplest control flow diagram is the DD path. DD can be thought of as belonging to the control flow diagram.
DD path definition:
Given a program written in an imperative language, its DD path graph is a directed graph, where nodes represent the DD paths of its program graph and edges represent the control flow between successive DD paths.
In fact, the DD path graph is a compressed graph in which 2-connected components are compressed into a single node corresponding to the 5DD path.
The latest topic in my Software Quality Assurance and Testing class has been learning how to read and write program graphs and DD path graphs. We also have been learning how to make DD path graph tables that correspond with the DD path graphs and the program graphs. Our most recent assignment was all about these topics. With the amount of work we have put into learning these, the assignment proved to not be too difficult for me. It is generally easy to know how to read the program graphs because pretty much every node corresponds with a single line of code in a program. Where it gets tricky is when the program has loops and if statements. Converting to a DD path graph from the program graph is also not too hard if you know what you are doing. A lot of nodes from the program graph can be clumped together into single nodes (like all the instance variables for example). Lastly, creating the DD path graph table takes in account both the program graph and the DD path graph, but again is not too difficult to read or fill out. The amount of time we have spend on these topics in class while doing in-class activities has helped me tremendously, because looking at the graphs without knowing what you are looking at can be daunting and confusing. As much as I enjoy being able to understand the graphs and make them if I want, it is even more enjoyable knowing that these topics can all be used in real life. It is always very reassuring when I try to research topics from class and they seem to be very prominently used in the real world with lots of examples and helpful sources to teach me about them. In this case, these topics all seem to be used a lot and also seem to be very helpful for those who have used them before to study how a program could be working (or how it could be failing).
In my operating systems class, we are learning about all of the important roles the operating system has in a computer. There are many managers that the operating system itself manages to keep everything running as smoothly as possible. One thing I am most interested in when thinking about operating systems is concurrency and multithreading. Most tasks are done serially, which is to say one at a time. This worked well back in the day when most computers had only one or two cores. Nowadays, however, CPUs usually have 4+ cores, and if all tasks an operating system does are serial, then 3/4 of the CPUs resources are not being utilized. This is where concurrency / parallelism / multithreading comes into play. With them, we allow programs/tasks to run simultaneously, using more than one CPU core, to do more work in less time.
The only problem with this is trying to write the programs/tasks to take advantage of more cores. I know when I first tried to create and run a simple C program that would use more than one core, it was very confusing on trying to have everything be worked on by a separate thread. Eventually, after some trial and error, I got it working, and it slowly started to make sense on how to write this type of program. In doing some very simple calculations like I was trying to do in my example program, there is not much performance to be gained from multithreading the program, but in much bigger programs with bigger calculations to be done, there is definitely some time that can be saved by multithreading the process.