View Javadoc
1   /**
2   
3   @file CalculatorAppTest.java
4   @brief This file contains the test cases for the CalculatorApp class.
5   @details This file includes test methods to validate the functionality of the CalculatorApp class. It uses JUnit 5
6            (Jupiter) for unit testing. None of these tests touch System.in: CalculatorApp#run takes its input only
7            from the argument array, so it never blocks and is safe to run from a script or CI pipeline.
8   */
9   package com.ucoruh.calculator;
10  
11  import static org.junit.jupiter.api.Assertions.assertEquals;
12  import static org.junit.jupiter.api.Assertions.assertTrue;
13  
14  import java.io.ByteArrayOutputStream;
15  import java.io.PrintStream;
16  import java.nio.charset.StandardCharsets;
17  
18  import org.junit.jupiter.api.AfterEach;
19  import org.junit.jupiter.api.BeforeEach;
20  import org.junit.jupiter.api.DisplayName;
21  import org.junit.jupiter.api.Nested;
22  import org.junit.jupiter.api.Test;
23  
24  /**
25  
26  @class CalculatorAppTest
27  @brief This class represents the test class for the CalculatorApp class.
28  @details The CalculatorAppTest class provides test methods to verify the behavior of CalculatorApp#run for normal
29           operation, boundary/edge input (missing or extra arguments) and invalid input (non-numeric operands,
30           unknown operator, division by zero).
31  @author ugur.coruh
32  */
33  class CalculatorAppTest {
34  
35    @Nested
36    @DisplayName("run: normal input")
37    class NormalInput {
38  
39      @Test
40      @DisplayName("adds two numbers")
41      void add() {
42        assertEquals("5", CalculatorApp.run(new String[] {"2", "+", "3"}));
43      }
44  
45      @Test
46      @DisplayName("subtracts two numbers")
47      void subtract() {
48        assertEquals("-1", CalculatorApp.run(new String[] {"2", "-", "3"}));
49      }
50  
51      @Test
52      @DisplayName("multiplies two numbers")
53      void multiply() {
54        assertEquals("12", CalculatorApp.run(new String[] {"3", "*", "4"}));
55      }
56  
57      @Test
58      @DisplayName("divides two numbers")
59      void divide() {
60        assertEquals("2.0", CalculatorApp.run(new String[] {"6", "/", "3"}));
61      }
62    }
63  
64    @Nested
65    @DisplayName("run: boundary / edge input")
66    class BoundaryInput {
67  
68      @Test
69      @DisplayName("no arguments prints usage instead of blocking")
70      void noArguments() {
71        assertEquals(CalculatorApp.USAGE, CalculatorApp.run(new String[] {}));
72      }
73  
74      @Test
75      @DisplayName("null argument array prints usage instead of throwing")
76      void nullArguments() {
77        assertEquals(CalculatorApp.USAGE, CalculatorApp.run(null));
78      }
79  
80      @Test
81      @DisplayName("too few arguments prints usage")
82      void tooFewArguments() {
83        assertEquals(CalculatorApp.USAGE, CalculatorApp.run(new String[] {"1", "+"}));
84      }
85  
86      @Test
87      @DisplayName("too many arguments prints usage")
88      void tooManyArguments() {
89        assertEquals(CalculatorApp.USAGE, CalculatorApp.run(new String[] {"1", "+", "2", "3"}));
90      }
91  
92      @Test
93      @DisplayName("Integer.MAX_VALUE operand is accepted")
94      void maxIntOperand() {
95        assertEquals("2147483647", CalculatorApp.run(new String[] {"2147483647", "+", "0"}));
96      }
97    }
98  
99    @Nested
100   @DisplayName("run: invalid input")
101   class InvalidInput {
102 
103     @Test
104     @DisplayName("non-numeric first operand is reported, not thrown")
105     void nonNumericFirstOperand() {
106       String result = CalculatorApp.run(new String[] {"abc", "+", "3"});
107       assertTrue(result.startsWith("Error: operands must be integers"));
108     }
109 
110     @Test
111     @DisplayName("non-numeric second operand is reported, not thrown")
112     void nonNumericSecondOperand() {
113       String result = CalculatorApp.run(new String[] {"3", "+", "xyz"});
114       assertTrue(result.startsWith("Error: operands must be integers"));
115     }
116 
117     @Test
118     @DisplayName("unknown operator is reported, not thrown")
119     void unknownOperator() {
120       String result = CalculatorApp.run(new String[] {"1", "%", "2"});
121       assertTrue(result.startsWith("Error: unknown operator"));
122     }
123 
124     @Test
125     @DisplayName("division by zero is reported, not thrown")
126     void divisionByZero() {
127       assertEquals("Error: division by zero", CalculatorApp.run(new String[] {"1", "/", "0"}));
128     }
129   }
130 
131   @Nested
132   @DisplayName("main: prints run()'s result and returns without reading stdin")
133   class MainMethod {
134 
135     private final PrintStream originalOut = System.out;
136     private ByteArrayOutputStream capturedOut;
137 
138     @BeforeEach
139     void redirectOut() {
140       capturedOut = new ByteArrayOutputStream();
141       System.setOut(new PrintStream(capturedOut, true, StandardCharsets.UTF_8));
142     }
143 
144     @AfterEach
145     void restoreOut() {
146       System.setOut(originalOut);
147     }
148 
149     @Test
150     @DisplayName("prints the computed result and exits without waiting for input")
151     void printsResult() {
152       // System.in is intentionally left untouched: main() must complete without
153       // ever reading from it.
154       CalculatorApp.main(new String[] {"4", "*", "5"});
155       assertEquals("20", capturedOut.toString(StandardCharsets.UTF_8).trim());
156     }
157 
158     @Test
159     @DisplayName("prints the usage message for invalid arguments and exits")
160     void printsUsageForInvalidArguments() {
161       CalculatorApp.main(new String[] {"not-enough-args"});
162       assertEquals(CalculatorApp.USAGE, capturedOut.toString(StandardCharsets.UTF_8).trim());
163     }
164   }
165 }