# Keksdose

Quelle: https://dev.inf.zone/exercises/10/cookie-jar/

![Sesame Street](https://dev.inf.zone/exercises/10/cookie-jar/giphy1.gif)
_Quelle: Sesame Street_

## Aufgabe

**Auf einen Blick**

- **Was:** Eine Klasse `Jar`, die eine [Keksdose](https://en.wikipedia.org/wiki/Cookie_jar) darstellt, und Tests dafür.
- **Dateien:** `jar.py` (die Klasse) und `test_jar.py` (Ihre Tests) in einem Ordner `jar`.
- **Klasse:** `Jar(capacity=12)` mit den Methoden `deposit(n)`, `withdraw(n)` und `__str__` sowie den Properties `capacity` und `size`.
- **Fehler:** Bei ungültigen Werten lösen Sie einen `ValueError` aus.
- **Tests:** mindestens vier Funktionen, deren Name mit `test_` beginnt, ausführbar mit `pytest test_jar.py`.

### So funktioniert die Keksdose

1. **Kapazität:** Beim Anlegen legt `capacity` fest, wie viele Kekse höchstens hineinpassen; ohne Angabe sind es 12. Ist `capacity` keine positive Ganzzahl (`int`), etwa `0`, `-5` oder `"ten"`, gibt es einen `ValueError`.
2. **Anfangs leer:** Eine neue Dose enthält 0 Kekse.
3. **Hineinlegen:** `deposit(n)` legt `n` Kekse hinein. Würde die Dose dadurch mehr Kekse enthalten, als `capacity` erlaubt, gibt es einen `ValueError`.
4. **Herausnehmen:** `withdraw(n)` nimmt `n` Kekse heraus. Sind weniger als `n` Kekse in der Dose, gibt es einen `ValueError`.
5. **Negative Anzahl:** Ist `n` bei `deposit` oder `withdraw` negativ, gibt es ebenfalls einen `ValueError`.
6. **Fehler ändern nichts:** Nach einem `ValueError` enthält die Dose so viele Kekse wie vorher.
7. **Anzeige:** `str(jar)` liefert für jeden Keks in der Dose ein `🍪`, bei einer leeren Dose also `""`.
8. **Abfragen:** `jar.capacity` liefert die Kapazität, `jar.size` die Zahl der Kekse in der Dose. Beide sind Properties: Man schreibt sie ohne Klammern und kann sie von außen nur lesen.

**Beispiel** mit einer Dose für 10 Kekse:

| Aufruf            | Ergebnis                                   | `jar.size` | `str(jar)`       |
| ----------------- | ------------------------------------------ | ---------- | ---------------- |
| `jar = Jar(10)`   | neue, leere Dose                           | 0          | `""`             |
| `jar.deposit(8)`  |                                            | 8          | `🍪🍪🍪🍪🍪🍪🍪🍪` |
| `jar.withdraw(3)` |                                            | 5          | `🍪🍪🍪🍪🍪`       |
| `jar.deposit(6)`  | `ValueError`: 5 + 6 = 11 ist mehr als 10    | 5          | `🍪🍪🍪🍪🍪`       |
| `jar.withdraw(6)` | `ValueError`: nur 5 Kekse in der Dose       | 5          | `🍪🍪🍪🍪🍪`       |
| `jar.deposit(5)`  | Dose ist voll                              | 10         | `🍪🍪🍪🍪🍪🍪🍪🍪🍪🍪` |
| `Jar(0)`          | `ValueError`: Kapazität muss positiv sein  |            |                  |

## Demo

Ein Hauptprogramm brauchen Sie nicht (Sie dürfen aber eines schreiben). Mehr als das hier gibt es also nicht vorzuführen:

![Sesame Street](https://dev.inf.zone/exercises/10/cookie-jar/giphy2.gif)
*Quelle: Sesame Street*

## Spezifikation

-   Implementieren Sie in `jar.py` im Ordner `jar` eine Klasse `Jar` mit genau diesem Aufbau. Die Parameter der Methoden dürfen Sie nicht ändern; eigene Methoden dürfen Sie ergänzen.

    ```python
    class Jar:
        def __init__(self, capacity=12):
            ...

        def __str__(self):
            ...

        def deposit(self, n):
            ...

        def withdraw(self, n):
            ...

        @property
        def capacity(self):
            ...

        @property
        def size(self):
            ...
    ```

-   `__init__` prüft die Kapazität nach Regel 1 und legt eine leere Dose an (Regel 2).
-   `deposit` und `withdraw` ändern die Zahl der Kekse nach den Regeln 3 bis 6.
-   `__str__` gibt die Kekse als String zurück (Regel 7).
-   `capacity` und `size` geben die Kapazität bzw. die aktuelle Zahl der Kekse zurück (Regel 8).
-   Schreiben Sie in `test_jar.py` mindestens vier Funktionen, deren Name mit `test_` beginnt und die Ihre Klasse zusammen gründlich testen (siehe [Testen](#testen)).

## Hilfestellung

Klicken Sie auf die folgenden Tipps, um einige Ratschläge zu erhalten. Versuchen Sie aber zunächst, selbst so weit wie möglich zu kommen.

**Ordner und Dateien anlegen**

Öffnen Sie VS Code entsprechend Ihrem [Setup](https://dev.inf.zone/extras/setup/), klicken Sie auf Ihr Terminalfenster und führen Sie `cd` aus. Die Eingabeaufforderung sollte ungefähr so aussehen:

```bash
$
```

Legen Sie dann den Ordner an, wechseln Sie hinein und öffnen Sie die beiden Dateien:

```bash
mkdir jar
cd jar
code jar.py
code test_jar.py
```

**Wo die Zahlen gespeichert werden**

Speichern Sie Kapazität und Zahl der Kekse in `__init__` in Instanzvariablen mit Unterstrich, etwa `self._capacity` und `self._size`. Die Properties `capacity` und `size` geben diese Werte dann zurück.

Ohne Unterstrich geht es nicht: `self.size = 0` würde versuchen, die Property `size` zu überschreiben, und Python bricht mit einem `AttributeError` ab (die Property hat keinen Setter).

**Fehler auslösen, Kekse anzeigen**

Einen Fehler lösen Sie mit `raise` aus; der Text in Klammern ist frei wählbar:

```python
if n < 0:
    raise ValueError("Cannot deposit a negative number of cookies.")
```

Ob ein Wert ein `int` ist, prüft `isinstance(capacity, int)`.

Den String aus Regel 7 bekommen Sie ohne Schleife: `"🍪" * 3` ergibt `"🍪🍪🍪"`.

**Gerüst für test_jar.py**

```python
import pytest
from jar import Jar

def test_init():
    ...

def test_str():
    jar = Jar()
    assert str(jar) == ""
    jar.deposit(1)
    assert str(jar) == "🍪"
    jar.deposit(11)
    assert str(jar) == "🍪🍪🍪🍪🍪🍪🍪🍪🍪🍪🍪🍪"

def test_deposit():
    ...

def test_withdraw():
    ...
```

Ob ein Aufruf den erwarteten `ValueError` auslöst, prüfen Sie mit `pytest.raises`. Der Test schlägt fehl, wenn im eingerückten Block *kein* `ValueError` auftritt:

```python
with pytest.raises(ValueError):
    Jar(-5)
```

## Testen

Schreiben Sie die Tests Schritt für Schritt in `test_jar.py` und führen Sie nach jedem Schritt

```bash
pytest test_jar.py
```

aus. Jede Funktion legt mit `Jar()` oder `Jar(10)` eine eigene Dose an.

| Testfunktion    | Was sie prüft                                                                                                   |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| `test_init`     | Kapazität einer neuen Dose (Standard 12 und selbst gewählt), `size` ist 0; `ValueError` bei ungültiger Kapazität |
| `test_str`      | `str(jar)` zeigt nach einigen `deposit`-Aufrufen die richtige Zahl Kekse                                         |
| `test_deposit`  | `size` entspricht der Zahl der hineingelegten Kekse; `ValueError`, wenn die Kapazität überschritten würde          |
| `test_withdraw` | `size` stimmt nach dem Herausnehmen; `ValueError`, wenn mehr Kekse herausgenommen werden sollen, als drin sind     |

Um `withdraw` zu testen, müssen erst Kekse in der Dose sein. Rufen Sie dafür im Test einfach vorher `deposit` auf. Die Aufruffolge aus dem [Beispiel](#so-funktioniert-die-keksdose) eignet sich als Vorlage.

### Korrektheit

Führen Sie in Ihrem Terminal den folgenden Befehl aus, um die Korrektheit Ihrer Arbeit zu überprüfen. Testen Sie Ihr Programm aber zuerst selbst.

```bash
check50 -l inf-zone/exercises/2026/jar
```

### Style

Führen Sie den folgenden Befehl aus, um den Stil Ihres Codes mit `style50` zu analysieren:

```bash
style50 jar.py
```

## Abgeben

Geben Sie im Ordner `jar` ab:

```bash
inf upload jar
```

Danach sehen Sie, welche Tests Ihr Programm besteht und ob die Übung für die Bonuspunkte zählt. Wie Sie `inf` installieren und sich anmelden, steht unter [Abgeben mit `inf`](https://dev.inf.zone/faq/uebung-solutions/#inf-installieren).

## Zum Weiterlesen: Methoden isoliert testen

Für die Lösung der Aufgabe nicht nötig.

Methoden zu testen ist oft aufwendiger als eigenständige Funktionen, weil Methoden den Zustand des Objekts (seine Instanzvariablen) verändern. Um `withdraw` zu testen, rufen Sie vorher `deposit` auf. Ist aber `deposit` fehlerhaft, schlägt womöglich auch der Test von `withdraw` fehl, obwohl `withdraw` richtig ist.

Deshalb simulieren Programmierer beim Testen den Zustand oft mit sogenannten [Mock-Objekten](https://de.wikipedia.org/wiki/Mock-Objekt). Mit Werkzeugen wie Pythons [Mock-Objektbibliothek](https://docs.python.org/3/library/unittest.mock.html) setzen sie den Zustand einer Instanz direkt, ohne eine andere Methode aufzurufen, und testen so jede Methode für sich.
