# Lab 02 : Unit tests with dependency mocking

# Useful commands 
```
mvn test 
mvn failsafe:integration-test
```

# 1. Mockito

## When to use

- Your class depends on an external system (APIs, databases, web services, file systems)

- You don't want your unit test to depend on them.

- The real dependency is slow or unreliable

- The dependency is hard to set up

<br>

**Without Mockito, a test might look like this:**

```java
IStockmarketService realService = new RemoteStockService();  //real API
StocksPortfolio portfolio = new StocksPortfolio(realService);
portfolio.addStock(new Stock("AAPL", 10));

//problem: depends on real Apple stock price right now!
//test result changes every second, may fail randomly.
double value = portfolio.totalValue();
```
<br>

**With Mockito:**

```java
@Mock IStockmarketService market;
when(market.lookUpPrice("AAPL")).thenReturn(100.0);

StocksPortfolio portfolio = new StocksPortfolio(market);
portfolio.addStock(new Stock("AAPL", 10));

double value = portfolio.totalValue();
assertEquals(1000.0, value); //stable
```

<br>

### When not to use Mockito

- For simple classes without dependencies, mocks are unnecessary.

- When you want to run integration tests ( really check DB connection, REST API call).

- When using mocks would make the test less readable than just writing a fake stub manually.


<br>

## 1.1 How to use

### Pom.xml dependency

```xml
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.20.0</version>
    <scope>test</scope>
</dependency>
```

(because im on java 21, i got the Mockito self-attaching warning, to fix this, configure Surefire)

```xml
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.20.0</version>
    <scope>test</scope>
</dependency>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.6.1</version>
    <executions>
        <execution>
            <goals>
                <goal>properties</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar}</argLine>
    </configuration>
</plugin>
```

<br>

## 1.2 Mockito + JUNit 5

```@ExtendWith(MockitoExtension.class):``` ties Mockito into JUnit 5’s lifecycle; @Mock fields are auto-initialized (use right before declaring the class)

```@Mock:``` creates a Mockito mock for the interface/class.

```when(mock.method(arg)).thenReturn(value):``` stubbing—define what the mock should return for a given input.

```verify(mock).method(arg):``` ensure the SUT made the expected calls.


**note:** before verify, make sure to assert if the results were as expected

<br>

**if unused stubbings are coded, Mockito will issue an error:**

```
org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
```

<br>

# 2. Alternate Assertion Libraries 

## 2.1 Hamcrest
```xml
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest</artifactId>
    <version>3.0</version>
    <scope>test</scope>
</dependency>
```

### Why Hamcrest?
- English-like syntax, 
- strong matchers, 
- extensible

But error messages are not as descriptive as AssertJ, and involves more typing (is more verbose)

<br>

## 2.2 AssertJ

```xml
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.27.6</version>
    <scope>test</scope>
</dependency>
```

### Why AssertJ?

- Fluent chaining and expressive
- Modern
- Rich API
- Descriptive error messages

But might feel like overkill for small or simple projects and has a big learning curve when coming from JUnit built in assertions

<br>

## 2.3 JUnit, Hamcrest, AssertJ

| **Purpose**              | **JUnit** | **Hamcrest** | **AssertJ** |
|---------------------------|-----------------------------|--------------|-------------|
| **Equality**              | `assertEquals(5, result);` | `assertThat(result, is(5));` | `assertThat(result).isEqualTo(5);` |
| **Boolean (true/false)**  | `assertTrue(flag);` <br> `assertFalse(flag);` | `assertThat(flag, is(true));` | `assertThat(flag).isTrue();` <br> `assertThat(flag).isFalse();` |
| **Null check**            | `assertNull(obj);` <br> `assertNotNull(obj);` | `assertThat(obj, nullValue());` <br> `assertThat(obj, notNullValue());` | `assertThat(obj).isNull();` <br> `assertThat(obj).isNotNull();` |
| **Floating-point (with delta)** | `assertEquals(3.14, value, 1e-3);` | `assertThat(value, closeTo(3.14, 1e-3));` | `assertThat(value).isCloseTo(3.14, within(1e-3));` |
| **Collection contains item** | `assertTrue(list.contains("A"));` | `assertThat(list, hasItem("A"));` | `assertThat(list).contains("A");` |
| **Collection size**       | `assertEquals(3, list.size());` | `assertThat(list, hasSize(3));` | `assertThat(list).hasSize(3);` |
| **Collection contents (order)** | `assertIterableEquals(Arrays.asList("A","B"), list);` | `assertThat(list, contains("A","B"));` | `assertThat(list).containsExactly("A", "B");` |
| **Collection contents (any order)** | *(manual sort or compare)* | `assertThat(list, containsInAnyOrder("B","A"));` | `assertThat(list).containsExactlyInAnyOrder("B", "A");` |
| **Exception thrown**      | `assertThrows(MyException.class, () -> foo());` | *(not great in Hamcrest)* | `assertThatThrownBy(() -> foo()).isInstanceOf(MyException.class);` |
| **String match**          | `assertTrue(str.startsWith("Hi"));` | `assertThat(str, startsWith("Hi"));` | `assertThat(str).startsWith("Hi");` |
| **Multiple conditions**   | `assertAll(() -> {...}, () -> {...});` | `assertThat(x, allOf(greaterThan(0), lessThan(10)));` | `assertThat(x).isGreaterThan(0).isLessThan(10);` |


<br>

# 3. Mock API Calls
During development it is good to use a mock API in case: 
- the team has not yet decided which HTTP Client to use;
- each API call is paid and the team wants to minimize costs at development time. 

<br>

## 3.1 Project Structure
**Interface httpClient**
```java
public interface ISimpleHttpClient {
    String doHttpGet(String url);
}
```
<br>

**Class Product**

```java
public Product(Integer id, String image, String description){
this.id = id;
this.image = image;
this.description = description;
}
```
<br>

**Service**

```java
public class ProductFinderService {

    private ISimpleHttpClient httpClient;
    private static final String API_PRODUCTS = "https://fakestoreapi.com/products/";

    public ProductFinderService(ISimpleHttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public Optional<Product> findProductDetails(Integer id) {
        String url = API_PRODUCTS + id;
        String json = httpClient.doHttpGet(url);

        if (json == null || json.isEmpty() || json.contains("Not Found")) {
            return Optional.empty();
        }

        Gson gson = new Gson();
        Product product = gson.fromJson(json, Product.class);
        
        return Optional.ofNullable(product);
    }
}
```

<br>

### Mock call to the API
In the tests, you create a fake JSON, as a potential response for the real API and mock a call to the real API

```java
//Fake API JSON response
String fakeJson = """
    {
        "id": 3,
        "title": "Mens Cotton Jacket",
        "price": 55.99,
        "description": "Great jacket",
        "image": "https://fakestoreapi.com/img/jacket.jpg",
        "category": "men's clothing"
    }
""";

//Mock the HTTP client to return that JSON
when(httpClient.doHttpGet("https://fakestoreapi.com/products/3"))
        .thenReturn(fakeJson);

//Execute the service method
Optional<Product> result = productFinderService.findProductDetails(3);

//Verify the parsing worked
assertTrue(result.isPresent());
Product product = result.get();
assertEquals(3, product.getId());
assertEquals("Mens Cotton Jacket", product.getTitle());

//Verify the mock was used
verify(httpClient).doHttpGet("https://fakestoreapi.com/products/3");
```

<br>
<br>

# 4. Connecting to a real API

## Failsafe maven plugin 
```xml
<dependency>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.5.4</version>
</dependency>
```

**Note:** Make sure Failsafe plugin and Surefire use the same updated version

<br>

## 4.1 Test file
When creating TEST files, **they should now use the IT Suffix** (it tells Failsafe to run it only in ```mvn failsafe:integration-test.```)

In the test file: 
```java
//setup the HTTP client and service
ISimpleHttpClient httpClient = new BasicHttpClient();
ProductFinderService service = new ProductFinderService(httpClient);

Optional<Product> result = service.findProductDetails(id);

    //assert you got a result
    assertTrue(result.isPresent());


    Product product = result.get();

    //assert the results are correct and not null
    assertEquals(3, product.getId());
    assertNotNull(product.getTitle());;
```

**Note:** in real API calls, verify() is not used, as it can only check interactions on mocks

<br>

## 4.2 Run Integration Test

```
mvn failsafe:integration-test
```

**This command will:**
- Build the project.
- Run all integration tests (but not the unit ones).
- Connect to the real API.

<br>

# 5. Class Notes

**The HTTP client should be  inside the service constructor, instead of being passed as a parameter**

If the ProductFinderService had instantiated the HTTP client internally, like this:

```java
public class ProductFinderService {
    private ISimpleHttpClient httpClient = new BasicHttpClient();
}
```

Then the service would be tightly coupled to BasicHttpClient.
We would no longer be able to inject any mock for testing, and every test would need to make real HTTP calls to the external API

<br>

### Some Maven commands
| Command | What it does | Runs Unit Tests? | Runs Integration Tests? |
|----------|---------------|------------------|--------------------------|
| `mvn test` | Compiles and runs unit tests only | yes | no |
| `mvn package` | Compiles, tests, and packages the project | yes | no |
| `mvn package -DskipTests=true` | Compiles and packages but skips test execution | no | no |
| `mvn failsafe:integration-test` | Runs integration tests after packaging | no | yes |
| `mvn install` | Builds, tests, runs integration tests, and installs locally | yes | yes |
