Repository: Lightning-AI/pytorch-lightning

Issue:
Test pass shouldn't require both test_step and test_end
##  🐛 Bug

`trainer.test(...)` requires implementation of both `test_step` and `test_end`, but the warning says you only need to implement either or both. 

https://github.com/PyTorchLightning/pytorch-lightning/blob/56dddf970825b1ad2b598c9b9b23a8a77add8964/pytorch_lightning/trainer/evaluation_loop.py#L291

### To Reproduce
Run `.test()` on any `LightningModule` with only `test_step` or `test_end` implemented. 

#### Code sample

``` python
import torch
from torch.nn import functional as F
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST

import pytorch_lightning as pl


class CoolSystem(pl.LightningModule):
    def __init__(self):
        super(CoolSystem, self).__init__()
        self.l1 = torch.nn.Linear(28 * 28, 10)

    def forward(self, x):
        return torch.relu(self.l1(x.view(x.size(0), -1)))

    def training_step(self, batch, batch_nb):
        x, y = batch
        y_hat = self.forward(x)
        loss = F.cross_entropy(y_hat, y)
        return {'loss': loss}

    def test_step(self, batch, batch_idx):
        x, y = batch
        y_hat = self.forward(x)
        return {'test_loss': F.cross_entropy(y_hat, y)}

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.02)

    @pl.data_loader
    def train_dataloader(self):
        return DataLoader(MNIST('./', train=True, download=True), batch_size=32)

    @pl.data_loader
    def test_dataloader(self):
        return DataLoader(MNIST('./', train=False, download=True), batch_size=32)


model = CoolSystem()
trainer = pl.Trainer(max_epochs=2, val_percent_check=1)
trainer.test(model)
```

### Expected behaviour

Test pass runs should run with either implemented or when at least `test_step` is.     



Discussion:
Hey, thanks for your contribution! Great first issue!
Great catch, would you send a PR? :robot:

Resolving patch:
diff --git a/pytorch_lightning/trainer/evaluation_loop.py b/pytorch_lightning/trainer/evaluation_loop.py
--- a/pytorch_lightning/trainer/evaluation_loop.py
+++ b/pytorch_lightning/trainer/evaluation_loop.py
@@ -301,8 +301,8 @@ def evaluate(self, model, dataloaders, max_batches, test=False):
 
     def run_evaluation(self, test=False):
         # when testing make sure user defined a test step
-        if test and not (self.is_overriden('test_step')):
-            m = '''You called `.test()` without defining model's `.test_step()`.
+        if test and not (self.is_overriden('test_step') or self.is_overriden('test_end')):
+            m = '''You called `.test()` without defining model's `.test_step()` or `.test_end()`.
                     Please define and try again'''
             raise MisconfigurationException(m)
 


Test patch:
diff --git a/tests/test_trainer.py b/tests/test_trainer.py
--- a/tests/test_trainer.py
+++ b/tests/test_trainer.py
@@ -782,5 +782,39 @@ def test_trainer_min_steps_and_epochs(tmpdir):
         trainer.current_epoch > 0, "Model did not train for at least min_steps"
 
 
+def test_testpass_overrides(tmpdir):
+    hparams = tutils.get_hparams()
+    from pytorch_lightning.utilities.debugging import MisconfigurationException
+
+    class TestModelNoEnd(LightningTestModelBase):
+        def test_step(self, *args, **kwargs):
+            return {}
+
+        def test_dataloader(self):
+            return self.train_dataloader()
+
+    class TestModelNoStep(LightningTestModelBase):
+        def test_end(self, outputs):
+            return {}
+
+        def test_dataloader(self):
+            return self.train_dataloader()
+
+    # Misconfig when neither test_step or test_end is implemented
+    with pytest.raises(MisconfigurationException):
+        model = LightningTestModelBase(hparams)
+        Trainer().test(model)
+
+    # No exceptions when one or both of test_step or test_end are implemented
+    model = TestModelNoStep(hparams)
+    Trainer().test(model)
+
+    model = TestModelNoEnd(hparams)
+    Trainer().test(model)
+
+    model = LightningTestModel(hparams)
+    Trainer().test(model)
+
+
 # if __name__ == '__main__':
 #     pytest.main([__file__])